responder

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 18, 2025 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package responder centralises JSON rendering, request parsing, and structured error payloads. See ExampleResponder_full for an end-to-end handler using custom classifiers and problem details.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ErrorClassifierFunc

type ErrorClassifierFunc func(err error) (status int, handled bool)

ErrorClassifierFunc inspects an error and returns the HTTP status that should be used for the response. The boolean indicates whether the error was classified and prevents the generic internal server handler from running.

type ProblemDetails

type ProblemDetails struct {
	Type      string `json:"type,omitempty"`
	Title     string `json:"title"`
	Status    int    `json:"status"`
	Detail    string `json:"detail,omitempty"`
	Instance  string `json:"instance,omitempty"`
	TraceID   string `json:"traceId,omitempty"`
	Timestamp string `json:"timestamp,omitempty"`
}

ProblemDetails aligns HTTP error responses with RFC 9457 problem documents.

type Responder

type Responder struct {
	// contains filtered or unexported fields
}

Responder centralises error handling, JSON rendering, and logging for HTTP handlers. It provides structured error payloads with correlation identifiers and consistent log records.

Example (Full)
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/drblury/apiweaver/responder"
)

func main() {
	errDuplicate := errors.New("project already exists")
	r := responder.NewResponder(
		responder.WithErrorClassifier(func(err error) (int, bool) {
			if errors.Is(err, errDuplicate) {
				return http.StatusConflict, true
			}
			return 0, false
		}),
	)

	store := make(map[string]struct{})
	handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		var body struct {
			Name string `json:"name"`
		}
		if !r.ReadRequestBody(w, req, &body) {
			return
		}
		if body.Name == "" {
			r.HandleBadRequestError(w, req, errors.New("name is required"))
			return
		}
		if _, exists := store[body.Name]; exists {
			r.HandleErrors(w, req, errDuplicate)
			return
		}
		store[body.Name] = struct{}{}
		r.RespondWithJSON(w, req, http.StatusCreated, map[string]string{"name": body.Name})
	})

	createReq := httptest.NewRequest(http.MethodPost, "/projects", strings.NewReader(`{"name":"weaver"}`))
	createRec := httptest.NewRecorder()
	handler.ServeHTTP(createRec, createReq)
	fmt.Println(createRec.Code)
	fmt.Println(strings.TrimSpace(createRec.Body.String()))

	dupReq := httptest.NewRequest(http.MethodPost, "/projects", strings.NewReader(`{"name":"weaver"}`))
	dupRec := httptest.NewRecorder()
	handler.ServeHTTP(dupRec, dupReq)

	var problem responder.ProblemDetails
	_ = json.Unmarshal(dupRec.Body.Bytes(), &problem)
	fmt.Println(problem.Status)
	fmt.Println(problem.Title)

}
Output:

201
{"name":"weaver"}
409
Conflict

func NewResponder

func NewResponder(opts ...ResponderOption) *Responder

NewResponder constructs a Responder with default status metadata and the global slog logger. Use ResponderOption functions to override specific behaviours.

func (*Responder) HandleAPIError

func (r *Responder) HandleAPIError(w http.ResponseWriter, req *http.Request, status int, err error, logMsg ...string)

HandleAPIError renders a structured JSON response for the supplied HTTP status and logs the payload using the configured logger.

func (*Responder) HandleBadRequestError

func (r *Responder) HandleBadRequestError(w http.ResponseWriter, req *http.Request, err error, logMsg ...string)

HandleBadRequestError reports client validation errors using HTTP 400.

func (*Responder) HandleErrors

func (r *Responder) HandleErrors(w http.ResponseWriter, req *http.Request, err error, msgs ...string)

HandleErrors inspects the supplied error using the configured classifier and emits an appropriate JSON response.

func (*Responder) HandleInternalServerError

func (r *Responder) HandleInternalServerError(w http.ResponseWriter, req *http.Request, err error, logMsg ...string)

HandleInternalServerError is a shortcut that reports a 500 status code.

func (*Responder) HandleUnauthorizedError

func (r *Responder) HandleUnauthorizedError(w http.ResponseWriter, req *http.Request, err error, logMsg ...string)

HandleUnauthorizedError reports authentication failures using HTTP 401.

func (*Responder) Logger

func (r *Responder) Logger() *slog.Logger

Logger returns the slog logger used internally by the responder.

func (*Responder) ReadRequestBody

func (r *Responder) ReadRequestBody(w http.ResponseWriter, req *http.Request, v any) bool

ReadRequestBody parses the request body into the provided value and handles malformed content by returning a JSON error response.

func (*Responder) RespondWithJSON

func (r *Responder) RespondWithJSON(w http.ResponseWriter, req *http.Request, status int, v any)

RespondWithJSON serialises the provided value and writes it to the response using the supplied status code.

type ResponderOption

type ResponderOption func(*Responder)

ResponderOption follows the functional options pattern used by NewResponder to configure optional collaborators.

func WithErrorClassifier

func WithErrorClassifier(classifier ErrorClassifierFunc) ResponderOption

WithErrorClassifier installs a classifier used by HandleErrors to derive the HTTP status code from returned errors.

func WithLogger

func WithLogger(logger *slog.Logger) ResponderOption

WithLogger injects a custom slog logger for error reporting and payload logging.

func WithStatusMetadata

func WithStatusMetadata(status int, meta StatusMetadata) ResponderOption

WithStatusMetadata overrides the error metadata used for a specific HTTP status code.

Example
package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/drblury/apiweaver/responder"
)

func main() {
	r := responder.NewResponder(
		responder.WithStatusMetadata(http.StatusUnauthorized, responder.StatusMetadata{
			Title:   "Missing token",
			LogMsg:  "authentication failed",
			TypeURI: "https://status.example.com/unauthorized",
		}),
	)

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/secret", nil)
	r.HandleUnauthorizedError(rec, req, errors.New("token expired"))

	fmt.Println(rec.Code)
	fmt.Println(strings.Contains(rec.Body.String(), "\"title\":\"Missing token\""))

}
Output:

401
true

type StatusMetadata

type StatusMetadata struct {
	TypeURI  string
	Title    string
	LogLevel slog.Level
	LogMsg   string
}

StatusMetadata allows callers to customise how particular HTTP status codes are logged and represented in error payloads.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL