Httpbun is an HTTP testing service that provides endpoints useful for testing HTTP clients, browsers, libraries, and API developer tools. It's heavily inspired by httpbin and is designed to help developers test and debug HTTP interactions.
The service provides various endpoints for:
- Testing different HTTP methods (GET, POST, PUT, DELETE, etc.)
- Inspecting request headers, cookies, and query parameters
- Testing authentication mechanisms
- Simulating redirects, delays, and streaming responses
- Testing various response status codes
httpbun/
├── main.go # Entry point, starts the server
├── server/ # Server setup and HTTP request handling
│ ├── server.go # Main server implementation
│ └── spec/ # Server configuration/specification
├── routes/ # Route handlers organized by feature
│ ├── routes.go # Main route registration
│ ├── method/ # HTTP method handlers (GET, POST, etc.)
│ ├── headers/ # Header inspection/setting handlers
│ ├── cookies/ # Cookie manipulation handlers
│ ├── auth/ # Authentication handlers (Basic, Bearer, Digest)
│ ├── redirect/ # Redirect testing handlers
│ ├── mix/ # Mixed response handlers
│ └── ... # Other route packages
├── ex/ # Exchange object (request/response wrapper)
│ └── exchange.go # Core Exchange type and methods
├── response/ # Response types and helpers
│ └── response.go # Response struct and helper functions
└── util/ # Utility functions
Route handlers are functions that take an *ex.Exchange and return a response.Response:
func handleMyRoute(ex *ex.Exchange) response.Response {
// Handler logic here
return response.Response{Body: "Hello"}
}Routes are registered using ex.NewRoute() which takes a regex pattern and a handler function:
var RouteList = []ex.Route{
ex.NewRoute("/my-route", handleMyRoute),
ex.NewRoute("/users/(?P<id>\\d+)", handleUser),
}Route patterns are regular expressions that can include named capture groups:
(?P<name>pattern)- Named capture group accessible viaex.Field("name")- Example:
/users/(?P<id>\\d+)matches/users/123and capturesidas"123"
- Each route package exports a
RouteListvariable containing[]ex.Route - Routes are collected in
routes.GetRoutes()which concatenates all route lists - The server matches incoming requests against routes in order
- When a route matches,
MatchAndLoadFields()extracts named groups intoex.fields - The handler function is called with the Exchange object
- The handler returns a
response.Responsewhich is sent viaex.Finish()
package mypackage
import (
"github.com/sharat87/httpbun/ex"
"github.com/sharat87/httpbun/response"
)
var RouteList = []ex.Route{
ex.NewRoute("/greet/(?P<name>\\w+)", handleGreet),
}
func handleGreet(ex *ex.Exchange) response.Response {
name := ex.Field("name")
return response.Response{
Body: map[string]string{
"message": "Hello, " + name,
},
}
}The Exchange object wraps the HTTP request and response, providing convenient methods for accessing request data and building responses.
Field(name string) string- Get a captured route parameter by name// Route: /users/(?P<id>\\d+) userId := ex.Field("id")
-
QueryParamInt(name string, value int) (int, error)- Get query parameter as integer with default valuepage, err := ex.QueryParamInt("page", 1) // Defaults to 1 if missing
-
QueryParamSingle(name string) (string, error)- Get single query parameter (errors if missing or multiple values)token, err := ex.QueryParamSingle("token")
FormParamSingle(name string) (string, error)- Get single form parameter (errors if missing or multiple values)email, err := ex.FormParamSingle("email")
-
HeaderValueLast(name string) string- Get the last value of a header (handles multiple values)contentType := ex.HeaderValueLast("Content-Type")
-
ExposableHeadersMap() map[string]any- Get all request headers as a map (excludes internal headers)headers := ex.ExposableHeadersMap()
-
BodyBytes() []byte- Get request body as bytes (capped at 10KB)body := ex.BodyBytes()
-
BodyString() string- Get request body as stringbody := ex.BodyString()
-
FindScheme() string- Get request scheme ("http" or "https")scheme := ex.FindScheme()
-
FullUrl() string- Get the full request URL including schemeurl := ex.FullUrl() // "https://example.com/path?query=value"
-
FindIncomingIPAddress() string- Get the client's IP addressip := ex.FindIncomingIPAddress()
Request *http.Request- Direct access to the underlying HTTP requestmethod := ex.Request.Method cookies := ex.Request.Cookies()
-
RedirectResponse(target string) *response.Response- Create a redirect responsereturn *ex.RedirectResponse("/new-path")
-
Finish(resp response.Response)- Send the response (called automatically by the server)// Usually not called directly - handler returns Response instead
MatchAndLoadFields(routePat regexp.Regexp) bool- Match route pattern and load fields (used internally)RoutedPath string- The request path after removing the server's path prefix
ServerSpec spec.Spec- Access server configuration/specificationprefix := ex.ServerSpec.PathPrefix
The response.Response struct has the following fields:
type Response struct {
Status int // HTTP status code (defaults to 200)
Header http.Header // Response headers
Cookies []http.Cookie // Cookies to set
Body any // Response body (string, []byte, or JSON-serializable)
Writer func(w BodyWriter) // Optional streaming writer function
}response.New(status int, header http.Header, body []byte) Response- Create a response with status, headers, and bodyresponse.BadRequest(message string, vars ...any) Response- Create a 400 Bad Request responsereturn response.BadRequest("Invalid parameter: %s", paramName)
The Body field accepts:
string- Sent as-is[]byte- Sent as raw bytesmap[string]anyor other JSON-serializable types - Automatically serialized to JSON withContent-Type: application/json
For streaming/chunked responses, use the Writer field:
return response.Response{
Status: 200,
Writer: func(w response.BodyWriter) {
w.Write("chunk1")
w.Write("chunk2")
},
}package example
import (
"net/http"
"github.com/sharat87/httpbun/ex"
"github.com/sharat87/httpbun/response"
)
var RouteList = []ex.Route{
ex.NewRoute("/api/users/(?P<id>\\d+)", handleUser),
}
func handleUser(ex *ex.Exchange) response.Response {
// Get route parameter
userID := ex.Field("id")
// Get query parameters
includeDetails, _ := ex.QueryParamInt("details", 0)
// Get header
authToken := ex.HeaderValueLast("Authorization")
// Validate
if authToken == "" {
return response.BadRequest("Missing Authorization header")
}
// Build response
return response.Response{
Status: http.StatusOK,
Header: http.Header{
"Content-Type": []string{"application/json"},
},
Body: map[string]any{
"id": userID,
"details": includeDetails == 1,
"authenticated": authToken != "",
},
}
}