Skip to content

Commit

Permalink
Merge pull request #2 from donseba/wip
Browse files Browse the repository at this point in the history
adding cors and content length middleware . some fixes throughout the code, ready to use it
  • Loading branch information
donseba authored Oct 4, 2024
2 parents f77cd36 + 3657fd6 commit 79d628c
Show file tree
Hide file tree
Showing 12 changed files with 803 additions and 166 deletions.
22 changes: 9 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ go-router is a lightweight, flexible, and idiomatic HTTP router for Go web appli

## Overview

- **Method-Based Routing**: Easily define routes for `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, and `HEAD` methods.
- **Method-Based Routing**: Easily define routes for `GET`, `POST`, `PUT`, `PATCH` and `DELETE` methods.
- **Route Grouping**: Organize routes under common base paths using groups.
- **Middleware Support**: Apply middleware functions globally or per group.
- **Custom 404, 405 and 500 Handlers**: Set custom handlers for NotFound (404) and MethodNotAllowed (405) responses.
- **Custom Status Handlers**: Set custom handlers for any possible status code.
- **Trailing Slash Handling**: Configure automatic redirection of trailing slashes.
- **Static File Serving**: Serve static files and directories seamlessly.
- **Built on Standard Library**: Utilizes Go's net/http package, ensuring performance and reliability.
Expand Down Expand Up @@ -60,16 +60,14 @@ Route Definition Methods
Define routes for specific HTTP methods.

- (*Router) Get(pattern string, handler http.HandlerFunc)
- (*Router) Head(pattern string, handler http.HandlerFunc)
- (*Router) Post(pattern string, handler http.HandlerFunc)
- (*Router) Put(pattern string, handler http.HandlerFunc)
- (*Router) Patch(pattern string, handler http.HandlerFunc)
- (*Router) Delete(pattern string, handler http.HandlerFunc)
- (*Router) Options(pattern string, handler http.HandlerFunc)v

#### Parameters

- **pattern string**: The URL pattern for the route. Patterns can include placeholders like {id}. A pattern that ends in “/” matches all paths that have it as a prefix, as always. To match the exact pattern including the trailing slash, end it with {$}, as in /exact/match/{$}.
- **pattern string**: The URL pattern for the route. Patterns can include placeholders like {id}. A pattern that ends in “/” matches all paths that have it as a prefix, as always. To match the exact pattern including the trailing slash, end it with `{$}`, as in `/exact/match/{$}`.
- **handler http.HandlerFunc**: The function to handle requests matching the pattern and method.

### Grouping Routes
Expand All @@ -95,8 +93,7 @@ Apply middleware functions to the router.

### Custom Handlers

v(*Router) NotFound(handler http.HandlerFunc)**: Set a custom handler for 404 Not Found responses.
- **(*Router) MethodNotAllowed(handler http.HandlerFunc)**: Set a custom handler for 405 Method Not Allowed responses.
- **(*Router) HandleStatus(http.StatusCode, handler http.HandlerFunc)**: Set a custom handler for any status code.

#### Parameters

Expand Down Expand Up @@ -182,14 +179,14 @@ admin.Get("/dashboard", adminDashboardHandler)
})
```

### Custom Handlers for 404 and 405 and 500 Responses
### Custom Handlers for response

Set custom handlers to provide consistent error responses.

```go
r.NotFound(notFoundHandler)
r.MethodNotAllowed(methodNotAllowedHandler)
r.InternalServerError(internalServerErrorHandler)
r.HandleStatus(http.StatusNotFound, notFoundHandler)
r.HandleStatus(http.StatusMethodNotAllowed, methodNotAllowedHandler)
r.HandleStatus(http.StatusInternalServerError,internalServerErrorHandler)
```

### Trailing Slash Handling
Expand Down Expand Up @@ -217,10 +214,9 @@ r.ServeFile("/favicon.ico", "./static/favicon.ico")

- **Pattern Matching**: Patterns not ending with a slash (/) are treated as exact matches, while patterns ending with a slash are treated as prefix matches.
- **Middleware Order**: Middleware functions are applied in the order they are added, wrapping subsequent middleware and the final handler.
- **Custom 404 and 405 Handling**: The router uses intercepting response writers to capture 404 and 405 responses from the underlying http.ServeMux and invoke custom handlers.
- **Custom Status Handling**: The router uses intercepting response writers to capture status code responses from the underlying http.ServeMux and invoke custom handlers.
- **Trailing Slash Redirection**: When enabled, requests with trailing slashes are redirected to the same path without the trailing slash.

## Future Improvements

- **Automatic OPTIONS Handling**: Provide automatic handling of OPTIONS requests.
- **Error Handling Enhancements**: Provide mechanisms for handling other HTTP status codes.
148 changes: 145 additions & 3 deletions example/openapi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ type (
ID string `json:"id"`
Name string `json:"name"`
}

Blog struct {
ID string `json:"id"`
Title string `json:"title"`
}
)

func main() {
Expand All @@ -26,23 +31,55 @@ func main() {
r.UseOpenapiDocs(true)
// Apply global middleware
r.Use(middleware.Timer)
r.Use(middleware.Recover)

// Serve static files
r.ServeFiles("/file/", http.Dir("./files"))
r.ServeFile("/favicon.ico", "./files/favicon.ico")

// Set custom handlers
r.NotFound(notFoundHandler)
r.MethodNotAllowed(methodNotAllowedHandler)
// Set custom handlers from methods
r.HandleStatus(http.StatusNotFound, notFoundHandler)
r.HandleStatus(http.StatusMethodNotAllowed, methodNotAllowedHandler)

r.RedirectTrailingSlash(true)

// set custom handler inlining
r.HandleStatus(http.StatusInternalServerError, func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "Custom 500 - Internal Server Error", http.StatusInternalServerError)
})

// Define routes
r.Get("/{$}", homeHandler, router.Docs{
Summary: "Home Page",
Description: "Displays the home page.",
Out: map[string]router.DocOut{
"200": {
ApplicationType: "text/html",
Description: "The home page.",
},
},
})
r.Get("/gopher", gopherHandler, router.Docs{
Summary: "Gopher Page",
Description: "Displays a gopher image.",
Out: map[string]router.DocOut{
"200": {
ApplicationType: "text/html",
Description: "The gopher image.",
},
},
})
r.Get("/panic", func(w http.ResponseWriter, req *http.Request) {
panic("Panic!")
}, router.Docs{
Summary: "Panic Page",
Description: "Generates a panic.",
Out: map[string]router.DocOut{
"500": {
ApplicationType: "text/plain",
Description: "Internal Server Error",
},
},
})
r.Post("/login", loginHandler)

Expand Down Expand Up @@ -91,13 +128,112 @@ func main() {
Object: User{},
},
},
Out: map[string]router.DocOut{
"200": {
ApplicationType: "application/json",
Description: "The updated user object.",
Object: User{},
},
},
})

r.Post("", func(w http.ResponseWriter, req *http.Request) {
_, _ = fmt.Fprintln(w, "Create User")
}, router.Docs{
Summary: "Create User",
Description: "Creates a new user.",
In: map[string]router.DocIn{
"application/json": {
Object: User{},
},
},
Out: map[string]router.DocOut{
"201": {
ApplicationType: "application/json",
Description: "The created user object.",
Object: User{},
},
},
})
})

r.Group("/blog", func(r *router.Router) {
r.Get("", func(w http.ResponseWriter, req *http.Request) {
_, _ = fmt.Fprintln(w, "Blog List Page")
}, router.Docs{
Summary: "Blog List",
Description: "Displays a list of blog posts.",
Out: map[string]router.DocOut{
"200": {
ApplicationType: "application/json",
Description: "The list of blog posts.",
Object: []Blog{},
},
},
})

r.Get("/{id}", func(w http.ResponseWriter, req *http.Request) {
blogID := req.PathValue("id")
_, _ = fmt.Fprintf(w, "Blog ID: %s", blogID)
}, router.Docs{
Summary: "Get Blog",
Description: "Retrieves a blog post by ID.",
Parameters: []router.Parameter{
{
Name: "id",
In: "path",
Description: "The ID of the blog post.",
Schema: &router.Schema{
Type: "string",
},
Required: true,
},
},
Out: map[string]router.DocOut{
"200": {
ApplicationType: "application/json",
Description: "The blog post object.",
Object: Blog{},
},
},
})

r.Post("", func(w http.ResponseWriter, req *http.Request) {
_, _ = fmt.Fprintln(w, "Create Blog")
}, router.Docs{
Summary: "Create Blog",
Description: "Creates a new blog post.",
In: map[string]router.DocIn{
"application/json": {
Object: Blog{},
},
},
Out: map[string]router.DocOut{
"201": {
ApplicationType: "application/json",
Description: "The created blog post.",
Object: Blog{},
},
},
})

r.Put("/{id}", func(w http.ResponseWriter, req *http.Request) {
_, _ = fmt.Fprintln(w, "Update Blog")
}, router.Docs{
Summary: "Update Blog",
Description: "Updates an existing blog post.",
In: map[string]router.DocIn{
"application/json": {
Object: Blog{},
},
},
Out: map[string]router.DocOut{
"200": {
ApplicationType: "application/json",
Description: "The updated blog post.",
Object: Blog{},
},
},
})
})

Expand All @@ -114,6 +250,12 @@ func main() {
}, router.Docs{
Summary: "OpenAPI Docs",
Description: "Displays the OpenAPI documentation.",
Out: map[string]router.DocOut{
"200": {
ApplicationType: "application/json",
Description: "The OpenAPI documentation.",
},
},
})

// Start the server
Expand Down
27 changes: 24 additions & 3 deletions example/simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,38 @@ func main() {

// Apply global middleware
r.Use(middleware.Timer)
r.Use(middleware.Recover)

// Serve static files
r.ServeFiles("/file/", http.Dir("./files"))
r.ServeFile("/favicon.ico", "./files/favicon.ico")

// Set custom handlers
r.NotFound(notFoundHandler)
r.MethodNotAllowed(methodNotAllowedHandler)
// Set custom handlers from methods
r.HandleStatus(http.StatusNotFound, notFoundHandler)
r.HandleStatus(http.StatusMethodNotAllowed, methodNotAllowedHandler)

// set custom handler inlining
r.HandleStatus(http.StatusInternalServerError, func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "Custom 500 - Internal Server Error", http.StatusInternalServerError)
})

// Define routes
r.Get("/{$}", homeHandler)
r.Get("/gopher", gopherHandler)
r.Post("/login", loginHandler)

r.Group("/users", func(r *router.Router) {
// custom middleware for this group
r.Use(func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
log.Print("[go-router] user middleware")

next.ServeHTTP(w, r)

}

return http.HandlerFunc(fn)
})
r.Get("", userListHandler)
r.Get("/{id}", userHandler)

Expand All @@ -42,6 +59,10 @@ func main() {
})
})

r.Get("/panic", func(w http.ResponseWriter, req *http.Request) {
panic("Panic!")
})

// Start the server
log.Println("Server is running at :3211")
err := http.ListenAndServe(":3211", r)
Expand Down
43 changes: 15 additions & 28 deletions interceptor.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
package router

import "net/http"
import (
"net/http"
)

// HeaderFlagDoNotIntercept defines a header that is (unfortunately) to be used
// as a flag of sorts, to denote to this routing engine to not intercept the
// response that is being written. It's an unfortunate artifact of an
// implementation detail within the standard library's net/http.ServeMux for how
// HTTP 404 and 405 responses can be customized, which requires writing a custom
// response writer and preventing the standard library from just writing it's
// own hard-coded response.
//
// See:
// - https://github.com/golang/go/issues/10123
// - https://github.com/golang/go/issues/21548
// - https://github.com/golang/go/issues/65648
//
// Author : https://github.com/Rican7 ( https://github.com/golang/go/issues/65648#issuecomment-2100088200 )
const HeaderFlagDoNotIntercept = "do_not_intercept"

type excludeHeaderWriter struct {
Expand All @@ -35,12 +23,9 @@ func (w *excludeHeaderWriter) WriteHeader(statusCode int) {
type routingStatusInterceptWriter struct {
http.ResponseWriter

intercept404 func() bool
intercept405 func() bool
intercept500 func() bool

statusCode int
intercepted bool
interceptMap map[int]func() bool
statusCode int
intercepted bool
}

func (w *routingStatusInterceptWriter) WriteHeader(statusCode int) {
Expand All @@ -49,13 +34,15 @@ func (w *routingStatusInterceptWriter) WriteHeader(statusCode int) {
}

w.statusCode = statusCode

if (w.intercept404() && statusCode == http.StatusNotFound) ||
(w.intercept405() && statusCode == http.StatusMethodNotAllowed) ||
(w.intercept500() && statusCode == http.StatusInternalServerError) {

w.intercepted = true
return
for code, fn := range w.interceptMap {
if w.intercepted {
return
}

if code == statusCode && fn() {
w.intercepted = true
return
}
}

w.ResponseWriter.WriteHeader(statusCode)
Expand Down
Loading

0 comments on commit 79d628c

Please sign in to comment.