Documentation
¶
Overview ¶
Package dhttp provides HTTP utilities for microservices with integrated logging, tracing, and error handling.
This package is designed for StreamingFast products and provides common HTTP handling patterns used across various microservices. It includes structured error responses, request validation, response writing utilities, and handler wrappers that automatically handle logging and tracing.
Handler Wrappers ¶
The package provides three main handler wrappers that simplify common HTTP patterns:
- JSONHandler: Wraps functions that return JSON responses
- RawHandler: Streams raw content from io.ReadCloser
- DirectHandler: Provides direct response writer control with error handling
Example using JSONHandler:
http.Handle("/api/users", dhttp.JSONHandler(func(r *http.Request) (any, error) {
// Your logic here
return users, nil
}))
Request Processing ¶
Extract and validate request data using ExtractRequest for URL/query parameters or ExtractJSONRequest for JSON payloads:
type UserRequest struct {
ID int `schema:"id"`
Name string `schema:"name"`
}
var req UserRequest
if err := dhttp.ExtractRequest(ctx, r, &req, dhttp.NoValidation); err != nil {
// Handle validation error
}
Response Writing ¶
Write responses with proper headers, logging, and tracing:
dhttp.WriteJSON(ctx, w, responseData) dhttp.WriteError(ctx, w, err) dhttp.WriteText(ctx, w, "Hello World")
Error Handling ¶
Create structured HTTP errors with proper status codes:
return dhttp.BadRequestError(ctx, nil, "invalid_input", "The request is invalid") return dhttp.NotFoundError(ctx, nil, "user_not_found", "User not found") return dhttp.InternalServerError(ctx, err, "database_error", "Database connection failed")
The package provides pre-made error functions for all standard HTTP status codes:
return dhttp.UnauthorizedError(ctx, nil, "auth_required", "Authentication required") return dhttp.ForbiddenError(ctx, nil, "access_denied", "Access denied") return dhttp.TooManyRequestsError(ctx, nil, "rate_limit", "Rate limit exceeded")
See errors.go for the complete list of available HTTP error functions (4xx and 5xx).
All HTTP errors are wrapped and can be easily used throughout your application. Error responses include trace IDs for debugging and follow a consistent JSON structure.
Utilities ¶
Additional utilities include:
- RealIP: Extract the real client IP from requests (Google Cloud Load Balancer aware)
- ForwardResponse: Forward HTTP responses from upstream services
- EmptyBody: Return empty response bodies from JSON handlers
The package integrates with OpenTelemetry for tracing and uses structured logging throughout all operations.
Index ¶
- Variables
- func DirectHandler(processor DirectHandlerProcessor) http.Handler
- func EmptyBody() any
- func ExtractJSONRequest(ctx context.Context, r *http.Request, request any, validator Validator) error
- func ExtractRequest(ctx context.Context, r *http.Request, request any, validator Validator) error
- func ForwardResponse(ctx context.Context, w http.ResponseWriter, response *http.Response)
- func JSONHandler(processor JSONHandlerProcessor) http.Handler
- func RawHandler(processor RawHandlerProcessor) http.Handler
- func RealIP(r *http.Request) string
- func WriteError(ctx context.Context, w http.ResponseWriter, err error)
- func WriteErrorResponse(ctx context.Context, w http.ResponseWriter, message string, err error)deprecated
- func WriteFromBytes(ctx context.Context, w http.ResponseWriter, bytes []byte)
- func WriteFromReader(ctx context.Context, w http.ResponseWriter, reader io.Reader)
- func WriteHTML(ctx context.Context, w http.ResponseWriter, htmlTpl *template.Template, ...)
- func WriteJSON(ctx context.Context, w http.ResponseWriter, v any)
- func WriteJSONString(ctx context.Context, w http.ResponseWriter, json string)
- func WriteText(ctx context.Context, w http.ResponseWriter, content string)
- func WriteTextf(ctx context.Context, w http.ResponseWriter, format string, arguments ...any)
- type DirectHandlerProcessor
- type ErrorResponse
- func ErrorFromStatus(status int, ctx context.Context, cause error, errorCode string, message any, ...) *ErrorResponse
- func InvalidJSONError(ctx context.Context, err error) *ErrorResponse
- func MissingBodyError(ctx context.Context) *ErrorResponse
- func RequestValidationError(ctx context.Context, errors url.Values) *ErrorResponse
- func ServiceNotAvailableError(ctx context.Context, cause error, serviceName string) *ErrorResponse
- func ToErrorResponse(ctx context.Context, err error) *ErrorResponse
- func UnexpectedError(ctx context.Context, cause error) *ErrorResponse
- type JSONHandlerProcessor
- type LoggingRoundTripper
- type NoOpValidator
- type RawHandlerProcessor
- type RequestValidator
- type Validator
Constants ¶
This section is empty.
Variables ¶
var ( BadRequestError = newErrorClass(http.StatusBadRequest) PaymentRequiredError = newErrorClass(http.StatusPaymentRequired) ForbiddenError = newErrorClass(http.StatusForbidden) NotFoundError = newErrorClass(http.StatusNotFound) MethodNotAllowedError = newErrorClass(http.StatusMethodNotAllowed) NotAcceptableError = newErrorClass(http.StatusNotAcceptable) ProxyAuthRequiredError = newErrorClass(http.StatusProxyAuthRequired) RequestTimeoutError = newErrorClass(http.StatusRequestTimeout) ConflictError = newErrorClass(http.StatusConflict) GoneError = newErrorClass(http.StatusGone) LengthRequiredError = newErrorClass(http.StatusLengthRequired) PreconditionFailedError = newErrorClass(http.StatusPreconditionFailed) RequestEntityTooLargeError = newErrorClass(http.StatusRequestEntityTooLarge) RequestURITooLongError = newErrorClass(http.StatusRequestURITooLong) UnsupportedMediaTypeError = newErrorClass(http.StatusUnsupportedMediaType) RequestedRangeNotSatisfiableError = newErrorClass(http.StatusRequestedRangeNotSatisfiable) ExpectationFailedError = newErrorClass(http.StatusExpectationFailed) TeapotError = newErrorClass(http.StatusTeapot) UnprocessableEntityError = newErrorClass(http.StatusUnprocessableEntity) LockedError = newErrorClass(http.StatusLocked) FailedDependencyError = newErrorClass(http.StatusFailedDependency) UpgradeRequiredError = newErrorClass(http.StatusUpgradeRequired) PreconditionRequiredError = newErrorClass(http.StatusPreconditionRequired) TooManyRequestsError = newErrorClass(http.StatusTooManyRequests) RequestHeaderFieldsTooLargeError = newErrorClass(http.StatusRequestHeaderFieldsTooLarge) )
var ( InternalServerError = newErrorClass(http.StatusInternalServerError) NotImplementedError = newErrorClass(http.StatusNotImplemented) BadGatewayError = newErrorClass(http.StatusBadGateway) GatewayTimeoutError = newErrorClass(http.StatusGatewayTimeout) StatusHTTPVersionNotSupportedError = newErrorClass(http.StatusHTTPVersionNotSupported) VariantAlsoNegotiatesError = newErrorClass(http.StatusVariantAlsoNegotiates) InsufficientStorageError = newErrorClass(http.StatusInsufficientStorage) LoopDetectedError = newErrorClass(http.StatusLoopDetected) NotExtendedError = newErrorClass(http.StatusNotExtended) NetworkAuthenticationRequiredError = newErrorClass(http.StatusNetworkAuthenticationRequired) )
var FowardResponse = ForwardResponse
Deprecated: Use ForwardResponse instead.
var NoValidation = &NoOpValidator{}
Functions ¶
func DirectHandler ¶
func DirectHandler(processor DirectHandlerProcessor) http.Handler
DirectHandler wraps a simpler `func(w http.ResponseWriter, r *http.Request) (err error)` processor.
This handler gives you full control over the response writer but with the added benefinit of having the error handling done for you.
func EmptyBody ¶
func EmptyBody() any
EmptyBody can be returned by a JSONHandlerProcessor to signal the JSON handler that the response body should be empty.
This must be used and not `nil` to avoid the JSON handler to write a `null` value.
func ExtractJSONRequest ¶
func ExtractRequest ¶
func ForwardResponse ¶
func JSONHandler ¶
func JSONHandler(processor JSONHandlerProcessor) http.Handler
JSONHandler wraps a simpler `func(r *http.Request) (out any, err error)` processor.
If the processor returns something as the `out` value, the `out` is serialized as JSON and return to the user.
If the processor returns an error insteand, the `err` value is written to the user using `dhttp.WriteError` call.
To have nothing returns as the body of the response, you can do:
return dhttp.EmptyBody(), nil
Which will return a 200 OK with an empty body.
func RawHandler ¶
func RawHandler(processor RawHandlerProcessor) http.Handler
RawHandler wraps a simpler `func(r *http.Request) (out io.ReadCloser, err error)` processor.
If the processor returns something as the `out` value, the `out` reader is fully transmitted to the user then close.
If the processor returns an error insteand, the `err` value is written to the user using `dhttp.WriteError` call.
func RealIP ¶
RealIP tries to determine the actual client remote IP that initiated the connection. This method is aware of Google Cloud Load Balancer and if the `X-Forwarded-For` exists and has 2 or more addresses, it will assume it's coming from Google Cloud Load Balancer.
When behind a Google Load Balancer, the only two values that we can be sure about are the `n - 2` and `n - 1` (so the last two values in the array). The very last value (`n - 1`) is the Google IP and the `n - 2` value is the actual remote IP that reached the load balancer.
When there is more than 2 IPs, all other values prior `n - 2` are those coming from the `X-Forwarded-For` HTTP header received by the load balancer directly, so something a client might have added manually. Since they are coming from an HTTP header and not from Google directly, they can be forged and cannot be trusted.
@see https://cloud.google.com/load-balancing/docs/https#x-forwarded-for_header
func WriteError ¶
func WriteError(ctx context.Context, w http.ResponseWriter, err error)
Write a generic error response to HTTP, the error is going to transformed to a ErrorResponse via a call to ToErrorResponse.
A generic `unable to fullfil request` message will be logged, use WriteErrorResponse to control which message will be logged.
func WriteErrorResponse
deprecated
WriteError writes the receiver error to HTTP and log it into a Zap logger at the same time with the right level based on the actual status code. The `WriteError` handles various type for the `err` parameter.
Deprecated: HTTP error handling has moved to dhttp(https://github.com/streamingfast/dhttp) package, which provides a more comprehensive and flexible approach to error handling in HTTP contexts.
func WriteFromBytes ¶
func WriteFromBytes(ctx context.Context, w http.ResponseWriter, bytes []byte)
func WriteFromReader ¶
func WriteJSONString ¶
func WriteJSONString(ctx context.Context, w http.ResponseWriter, json string)
func WriteTextf ¶
Types ¶
type DirectHandlerProcessor ¶
type DirectHandlerProcessor = func(w http.ResponseWriter, r *http.Request) (err error)
type ErrorResponse ¶
type ErrorResponse struct {
Code string `json:"code"`
TraceID string `json:"trace_id"`
Status int `json:"-"`
Message string `json:"message"`
Details map[string]any `json:"details,omitempty"`
Causer error `json:"-"`
}
func ErrorFromStatus ¶
func ErrorFromStatus(status int, ctx context.Context, cause error, errorCode string, message any, keyvals ...any) *ErrorResponse
ErrorFromStatus can be used to programmatically route the right status to one of the HTTP error class above
func InvalidJSONError ¶
func InvalidJSONError(ctx context.Context, err error) *ErrorResponse
func MissingBodyError ¶
func MissingBodyError(ctx context.Context) *ErrorResponse
func RequestValidationError ¶
func RequestValidationError(ctx context.Context, errors url.Values) *ErrorResponse
func ServiceNotAvailableError ¶
func ServiceNotAvailableError(ctx context.Context, cause error, serviceName string) *ErrorResponse
ServiceNotAvailableError represents a failure at the transport layer to reach a given micro-service. Note that while `serviceName` is required, it's not directly available to final response for now, will probably encrypt it into an opaque string if you ever make usage of it
func ToErrorResponse ¶
func ToErrorResponse(ctx context.Context, err error) *ErrorResponse
ToErrorResponse turns a plain `error` interface into a proper `ErrorResponse` object. It does so with the following rules:
- If `err` is already an `ErrorResponse`, turns it into such and returns it. - If `err` was wrapped, find the most cause which is an `ErrorResponse` and returns it. - If `err` is a status.Status (or one that was wrapped), convert it to an ErrorResponse - Otherwise, return an `UnexpectedError` with the cause sets to `err` received.
func UnexpectedError ¶
func UnexpectedError(ctx context.Context, cause error) *ErrorResponse
func (*ErrorResponse) Cause ¶
func (e *ErrorResponse) Cause() error
func (*ErrorResponse) Error ¶
func (e *ErrorResponse) Error() string
func (*ErrorResponse) ResponseStatus ¶
func (e *ErrorResponse) ResponseStatus() int
func (*ErrorResponse) Unwrap ¶
func (e *ErrorResponse) Unwrap() error
type LoggingRoundTripper ¶
type LoggingRoundTripper struct {
// contains filtered or unexported fields
}
func NewLoggingRoundTripper ¶
func NewLoggingRoundTripper(logger *zap.Logger, tracer logging.Tracer, next http.RoundTripper) *LoggingRoundTripper
NewLoggingRoundTripper create a wrapping `http.RoundTripper` aware object that intercepts the request as well as the response and logs them to the specified logger according to some rules if the debug and tracing level are enabled or not.
If the received `next` argument is set as `nil`, the `http.DefaultTransport` value will be used as the actual transport handler.
If debug is enabled, a one-line Request log containing HTTP method, URL and Headers is logged, and if tracing is enabled, the full request is dumped to the logger, in a multi-line log.
If debug is enabled, a one-line Response log containing HTTP status and body length is logged, and if tracing is enabled, the full request is dumped to the logger, in a multi-line log.
type NoOpValidator ¶
type NoOpValidator struct{}
type RawHandlerProcessor ¶
type RawHandlerProcessor = func(r *http.Request) (out io.ReadCloser, err error)
type RequestValidator ¶
type RequestValidator struct {
// contains filtered or unexported fields
}
func NewJSONRequestValidator ¶
func NewJSONRequestValidator(rules validator.Rules, options ...validator.Option) *RequestValidator
func NewRequestValidator ¶
func NewRequestValidator(rules validator.Rules, options ...validator.Option) *RequestValidator