httpfun

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Dec 16, 2025 License: MIT Imports: 16 Imported by: 2

Documentation

Overview

Package httpfun provides utilities for creating HTTP handlers from wrapped functions.

Overview

The httpfun package enables you to convert any wrapped function into an HTTP handler. It handles request argument parsing, function execution, result formatting, and error handling.

Basic Usage

Convert a function to an HTTP handler:

func Calculate(ctx context.Context, operation string, a, b int) (int, error) {
    switch operation {
    case "add":
        return a + b, nil
    case "multiply":
        return a * b, nil
    default:
        return 0, fmt.Errorf("unknown operation: %s", operation)
    }
}

func main() {
    wrapper := function.MustReflectWrapper("Calculate", Calculate)
    handler := httpfun.Handler(
        httpfun.RequestQueryArgs,     // Parse args from query params
        wrapper,
        httpfun.RespondJSON,          // Respond with JSON
    )

    http.Handle("/calculate", handler)
    http.ListenAndServe(":8080", nil)
}

Usage:

GET /calculate?operation=add&a=5&b=3
Response: 8

POST /calculate with JSON: {"operation":"multiply","a":5,"b":3}
Response: 15

Request Argument Parsing

Extract function arguments from various HTTP request sources:

// From query parameters
httpfun.RequestQueryArgs
httpfun.RequestQueryArg("argName")

// From JSON body
httpfun.RequestBodyJSONFieldsAsArgs

// From headers
httpfun.RequestHeaderArg("Authorization")
httpfun.RequestHeadersAsArgs(map[string]string{
    "Authorization": "token",
    "User-Agent": "userAgent",
})

// From form data
httpfun.RequestMultipartFormArgs

// From environment variables
httpfun.RequestArgFromEnvVar("API_KEY", "apiKey")

// Constant values
httpfun.ConstRequestArg("version", "1.0")

// Merge multiple sources (later sources override earlier ones)
httpfun.MergeRequestArgs(
    httpfun.ConstRequestArg("defaultValue", "abc"),
    httpfun.RequestQueryArgs,
    httpfun.RequestBodyJSONFieldsAsArgs,
)

Response Writers

Format function results as HTTP responses:

// JSON response (default)
httpfun.RespondJSON

// JSON with named fields
httpfun.RespondJSONObject("result", "error")

// XML response
httpfun.RespondXML

// Plain text
httpfun.RespondPlaintext

// HTML
httpfun.RespondHTML

// Binary data with specific content type
httpfun.RespondBinary("application/pdf")

// Auto-detect content type
httpfun.RespondDetectContentType

// Static responses
httpfun.RespondStaticHTML("<html>...</html>")
httpfun.RespondStaticJSON(`{"status":"ok"}`)

// Redirects
httpfun.RespondRedirect("/success")
httpfun.RespondRedirectFunc(func(r *http.Request) (string, error) {
    return "/user/" + getUserID(r), nil
})

Custom Response Writers

Create custom response formatting:

customWriter := httpfun.ResultsWriterFunc(func(
    results []any,
    err error,
    w http.ResponseWriter,
    r *http.Request,
) error {
    if err != nil {
        return err // Let error handler deal with it
    }

    // Custom formatting logic
    w.Header().Set("Content-Type", "text/plain")
    fmt.Fprintf(w, "Success: %v", results[0])
    return nil
})

Error Handling

Customize error responses:

httpfun.HandleError = func(err error, w http.ResponseWriter, r *http.Request) {
    log.Printf("Error: %v", err)
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}

Or provide custom error handlers per endpoint:

handler := httpfun.Handler(
    httpfun.RequestQueryArgs,
    wrapper,
    httpfun.RespondJSON,
    customErrorHandler, // Handles errors for this endpoint only
)

Configuration

Global configuration options:

// Pretty-print JSON responses (default: true)
httpfun.PrettyPrint = true
httpfun.PrettyPrintIndent = "  "

// Catch panics in handlers (default: true)
httpfun.CatchHandlerPanics = true

// Custom error handler (default uses go-httpx/httperr)
httpfun.HandleError = func(err error, w http.ResponseWriter, r *http.Request) {
    // Custom error handling
}

Advanced Examples

## REST API with Multiple Endpoints

type UserService struct{}

func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) { ... }
func (s *UserService) CreateUser(ctx context.Context, name, email string) (*User, error) { ... }
func (s *UserService) DeleteUser(ctx context.Context, id string) error { ... }

func main() {
    svc := &UserService{}

    http.Handle("/user", httpfun.Handler(
        httpfun.MergeRequestArgs(
            httpfun.ConstRequestArg("id", ""),
            httpfun.RequestQueryArgs,
        ),
        function.MustReflectWrapper("GetUser", svc.GetUser),
        httpfun.RespondJSON,
    ))

    http.Handle("/user/create", httpfun.Handler(
        httpfun.RequestBodyJSONFieldsAsArgs,
        function.MustReflectWrapper("CreateUser", svc.CreateUser),
        httpfun.RespondJSON,
    ))

    http.ListenAndServe(":8080", nil)
}

## File Upload Handler

func ProcessFile(ctx context.Context, fileData string) (string, error) {
    // Process the file
    return "processed", nil
}

handler := httpfun.Handler(
    httpfun.RequestBodyAsArg("fileData"),
    function.MustReflectWrapper("ProcessFile", ProcessFile),
    httpfun.RespondPlaintext,
)

## Binary Response Handler

func GeneratePDF(ctx context.Context, title string) ([]byte, error) {
    // Generate PDF
    return pdfBytes, nil
}

handler := httpfun.Handler(
    httpfun.RequestQueryArgs,
    function.MustReflectWrapper("GeneratePDF", GeneratePDF),
    httpfun.RespondBinary("application/pdf"),
)

Best Practices

  • Always include context.Context as the first parameter for cancellation support
  • Return errors as the last result for proper error handling
  • Use MergeRequestArgs to combine multiple argument sources
  • Configure error handling before starting the server
  • Enable panic recovery (CatchHandlerPanics) in production
  • Use appropriate content types for your responses
  • Validate inputs in your functions, not in request parsers
  • Log errors in your HandleError function for debugging

Performance Considerations

  • Request argument parsing happens on every request
  • JSON marshaling can be expensive for large responses
  • Consider disabling PrettyPrint in production for performance
  • Use binary responses for large files instead of base64 encoding
  • Cache wrapped functions instead of creating them per request

Thread Safety

The package-level configuration variables (PrettyPrint, HandleError, etc.) are not thread-safe. Set them once during initialization before handling requests. Handler functions are safe for concurrent use.

Index

Constants

This section is empty.

Variables

View Source
var (
	// PrettyPrint controls whether JSON and XML responses are formatted with indentation.
	// Set to false in production for smaller responses and better performance.
	// Default: true
	PrettyPrint = true

	// PrettyPrintIndent is the indentation string used when PrettyPrint is true.
	// Default: "  " (two spaces)
	PrettyPrintIndent = "  "

	// CatchHandlerPanics controls whether panics in HTTP handlers are recovered.
	// When true, panics are caught and converted to HTTP errors via HandleError.
	// When false, panics will crash the server (useful for debugging).
	// Default: true
	//
	// WARNING: This is a global setting. Set it once during initialization.
	// Not thread-safe to modify after handlers start running.
	CatchHandlerPanics = true

	// HandleError is called when an error occurs during request handling.
	// It writes the error to the HTTP response.
	//
	// The default implementation uses github.com/ungerik/go-httpx/httperr.DefaultHandler,
	// which provides sensible error responses with proper HTTP status codes.
	//
	// Customize this to implement your own error handling strategy:
	//
	//	httpfun.HandleError = func(err error, w http.ResponseWriter, r *http.Request) {
	//	    log.Printf("HTTP error: %v", err)
	//	    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	//	}
	//
	// WARNING: This is a global setting. Set it once during initialization.
	// Not thread-safe to modify after handlers start running.
	HandleError = func(err error, response http.ResponseWriter, request *http.Request) {
		if err != nil {
			httperr.DefaultHandler.HandleError(err, response, request)
		}
	}
)
View Source
var RespondNothing respondNothing

Functions

func DetectContentType

func DetectContentType(data []byte) string

DetectContentType tries to detect the MIME content-type of data, or returns "application/octet-stream" if none could be identified.

func Handler

func Handler(getArgs RequestArgsFunc, function function.CallWithNamedStringsWrapper, resultsWriter ResultsWriter, errHandlers ...httperr.Handler) http.HandlerFunc

Handler creates an http.HandlerFunc from a wrapped function.

Parameters:

  • getArgs: Extracts function arguments from the HTTP request (can be nil for no args)
  • function: The wrapped function to execute
  • resultsWriter: Formats and writes results to the response (can be nil for no response)
  • errHandlers: Optional custom error handlers (uses global HandleError if empty)

The handler:

  • Recovers from panics if CatchHandlerPanics is true
  • Parses arguments from the request using getArgs
  • Executes the wrapped function with request.Context()
  • Writes results using resultsWriter
  • Handles errors via errHandlers or global HandleError

Example:

func Calculate(ctx context.Context, a, b int) (int, error) {
    return a + b, nil
}

handler := httpfun.Handler(
    httpfun.RequestQueryArgs,
    function.MustReflectWrapper("Calculate", Calculate),
    httpfun.RespondJSON,
)
http.Handle("/calculate", handler)

Usage: GET /calculate?a=5&b=3 returns: 8

func HandlerNoWrapper

func HandlerNoWrapper(function func(context.Context) ([]byte, error), resultsWriter ResultsWriter, errHandlers ...httperr.Handler) http.HandlerFunc

HandlerNoWrapper creates an http.HandlerFunc from a simple function without using wrappers. The function must have the signature: func(context.Context) ([]byte, error)

This is useful for simple handlers that don't need argument parsing, or when you want to handle the request directly.

Parameters:

  • function: Function returning response bytes
  • resultsWriter: Formats and writes the bytes to the response
  • errHandlers: Optional custom error handlers

Example:

func GetStatus(ctx context.Context) ([]byte, error) {
    return []byte(`{"status":"ok"}`), nil
}

http.Handle("/status", httpfun.HandlerNoWrapper(
    GetStatus,
    httpfun.RespondJSON,
))

func RequestBodyJSONFieldsAsArgs

func RequestBodyJSONFieldsAsArgs(request *http.Request) (map[string]string, error)

RequestBodyJSONFieldsAsArgs returns a RequestArgsFunc that parses the body of the request as JSON object with the object field names as argument names and the field values as argument values.

func RequestMultipartFormArgs

func RequestMultipartFormArgs(request *http.Request) (map[string]string, error)

RequestMultipartFormArgs returns the multipart form values of the request as string map. If a form field has multiple values, they are joined with ";".

func RequestQueryArgs

func RequestQueryArgs(request *http.Request) (map[string]string, error)

RequestQueryArgs returns the query params of the request as string map. If a query param has multiple values, they are joined with ";".

Types

type RequestArgsFunc

type RequestArgsFunc func(*http.Request) (map[string]string, error)

RequestArgsFunc is a function type that returns a map of argument names and values for a given HTTP request.

func ConstRequestArg

func ConstRequestArg(name, value string) RequestArgsFunc

ConstRequestArg returns a RequestArgsFunc that returns a constant value for an argument name.

func ConstRequestArgs

func ConstRequestArgs(args map[string]string) RequestArgsFunc

ConstRequestArgs returns a RequestArgsFunc that returns a constant map of argument names and values.

func MergeRequestArgs

func MergeRequestArgs(getters ...RequestArgsFunc) RequestArgsFunc

MergeRequestArgs returns a RequestArgsFunc that merges the arguments of the given getters. Later getters overwrite earlier ones.

func RequestArgFromEnvVar

func RequestArgFromEnvVar(envVar, argName string) RequestArgsFunc

RequestArgFromEnvVar returns a RequestArgsFunc that returns the value of the environment variable envVar as the value of the argument argName.

An error is returned if the environment variable is not set.

func RequestBodyAsArg

func RequestBodyAsArg(argName string) RequestArgsFunc

RequestBodyAsArg returns a RequestArgsFunc that returns the body of the request as the value of the argument argName.

func RequestHeaderArg

func RequestHeaderArg(headerKeyArgName string) RequestArgsFunc

RequestHeaderArg returns a RequestArgsFunc that returns the value of the header headerKeyArgName as the value of the argument headerKeyArgName.

func RequestHeaderAsArg

func RequestHeaderAsArg(headerKey, argName string) RequestArgsFunc

RequestHeaderAsArg returns a RequestArgsFunc that returns the value of the header headerKey as the value of the argument argName.

func RequestHeadersAsArgs

func RequestHeadersAsArgs(headerToArg map[string]string) RequestArgsFunc

RequestHeadersAsArgs returns a RequestArgsFunc that returns the values of the request headers as argument values. The keys of headerToArg are the header keys and the values are the argument names.

func RequestQueryArg

func RequestQueryArg(queryKeyArgName string) RequestArgsFunc

RequestQueryArg returns a RequestArgsFunc that returns the value of the query param queryKeyArgName as the value of the argument queryKeyArgName.

func RequestQueryAsArg

func RequestQueryAsArg(queryKey, argName string) RequestArgsFunc

RequestQueryAsArg returns a RequestArgsFunc that returns the value of the query param queryKey as the value of the argument argName.

type RespondRedirect

type RespondRedirect string

RespondRedirect implements HTTPResultsWriter and http.Handler with for a redirect URL string. The redirect will be done with HTTP status code 302: Found.

func (RespondRedirect) ServeHTTP

func (re RespondRedirect) ServeHTTP(response http.ResponseWriter, request *http.Request)

func (RespondRedirect) WriteResults

func (re RespondRedirect) WriteResults(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error

type RespondRedirectFunc

type RespondRedirectFunc func(request *http.Request) (url string, err error)

RespondRedirectFunc implements HTTPResultsWriter and http.Handler with a function that returns the redirect URL. The redirect will be done with HTTP status code 302: Found.

func (RespondRedirectFunc) ServeHTTP

func (f RespondRedirectFunc) ServeHTTP(response http.ResponseWriter, request *http.Request)

func (RespondRedirectFunc) WriteResults

func (f RespondRedirectFunc) WriteResults(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error

type RespondStaticHTML

type RespondStaticHTML string

func (RespondStaticHTML) ServeHTTP

func (html RespondStaticHTML) ServeHTTP(response http.ResponseWriter, _ *http.Request)

func (RespondStaticHTML) WriteResults

func (html RespondStaticHTML) WriteResults(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error

type RespondStaticJSON

type RespondStaticJSON string

func (RespondStaticJSON) ServeHTTP

func (json RespondStaticJSON) ServeHTTP(response http.ResponseWriter, _ *http.Request)

func (RespondStaticJSON) WriteResults

func (json RespondStaticJSON) WriteResults(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error

type RespondStaticPlaintext

type RespondStaticPlaintext string

func (RespondStaticPlaintext) ServeHTTP

func (text RespondStaticPlaintext) ServeHTTP(response http.ResponseWriter, _ *http.Request)

func (RespondStaticPlaintext) WriteResults

func (text RespondStaticPlaintext) WriteResults(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error

type RespondStaticXML

type RespondStaticXML string

func (RespondStaticXML) ServeHTTP

func (xml RespondStaticXML) ServeHTTP(response http.ResponseWriter, _ *http.Request)

func (RespondStaticXML) WriteResults

func (xml RespondStaticXML) WriteResults(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error

type ResultsWriter

type ResultsWriter interface {
	// WriteResults writes the results and optionally the resultErr to the response.
	// If the method does not handle the resultErr then it should return it
	// so it can be handled by the next writer in the chain.
	WriteResults(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error
}

ResultsWriter implementations write the results of a function call to an HTTP response.

func RespondContentType

func RespondContentType(contentType string) ResultsWriter

type ResultsWriterFunc

type ResultsWriterFunc func(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error
var RespondDetectContentType ResultsWriterFunc = func(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error {

	if resultErr != nil || request.Context().Err() != nil {
		return resultErr
	}
	if len(results) != 1 {
		return fmt.Errorf("RespondDetectContentType needs 1 result, got %d", len(results))
	}
	data, ok := results[0].([]byte)
	if !ok {
		return fmt.Errorf("RespondDetectContentType needs []byte result, got %T", results[0])
	}

	response.Header().Add("Content-Type", DetectContentType(data))
	_, err := response.Write(data)
	return err
}
var RespondHTML ResultsWriterFunc = func(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error {

	if resultErr != nil || request.Context().Err() != nil {
		return resultErr
	}
	var buf bytes.Buffer
	for _, result := range results {
		if b, ok := result.([]byte); ok {
			buf.Write(b)
		} else {
			fmt.Fprint(&buf, result)
		}
	}
	response.Header().Add("Content-Type", contenttype.HTML)
	_, err := response.Write(buf.Bytes())
	return err
}
var RespondJSON ResultsWriterFunc = func(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error {

	if resultErr != nil || request.Context().Err() != nil {
		return resultErr
	}

	if len(results) == 0 {
		return nil
	}

	var r any
	if len(results) == 1 {

		r = results[0]
	} else {

		r = results
	}
	j, err := encodeJSON(r)
	if err != nil {
		return err
	}
	response.Header().Set("Content-Type", contenttype.JSON)
	_, err = response.Write(j)
	return err
}

RespondJSON writes the results of a function call as a JSON to the response. If the function returns one non-error result then it is marshalled as is. If the function returns multiple results then they are marshalled as a JSON array. If the function returns an resultErr then it is returned by this method, so it can be handled by the next writer in the chain. Any resultErr is not handled and will be returned by this method.

var RespondPlaintext ResultsWriterFunc = func(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error {

	if resultErr != nil || request.Context().Err() != nil {
		return resultErr
	}
	var buf bytes.Buffer
	fmt.Fprint(&buf, results...)
	response.Header().Add("Content-Type", contenttype.PlainText)
	_, err := response.Write(buf.Bytes())
	return err
}

RespondPlaintext writes the results of a function call as a plaintext to the response using fmt.Fprint to format the results. Spaces are added between results when neither is a string. Any resultErr is not handled and will be returned by this method.

var RespondXML ResultsWriterFunc = func(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error {

	if resultErr != nil || request.Context().Err() != nil {
		return resultErr
	}
	var buf []byte
	for i, result := range results {
		if i > 0 {
			buf = append(buf, '\n')
		}
		b, err := encodeXML(result)
		if err != nil {
			return err
		}
		buf = append(buf, b...)
	}
	response.Header().Set("Content-Type", contenttype.XML)
	_, err := response.Write(buf)
	return err
}

func RespondBinary

func RespondBinary(contentType string) ResultsWriterFunc

RespondBinary responds with contentType using the binary data from results of type []byte, string, or io.Reader. Any resultErr is not handled and will be returned by this method.

func RespondJSONObject

func RespondJSONObject(resultKeys ...string) ResultsWriterFunc

RespondJSONObject writes the results of a function call as a JSON object to the response. The resultKeys are the keys of the JSON object, naming the function results in order.

If the last result is an error and the resultKeys don't have a key for it then the error is returned unhandled if not nil. If there is a result key for the error then the error is marshalled as JSON string and not returned.

An error is returned if the number of results does not match the number of resultKeys or number of resultKeys minus one if the last result is an error.

func (ResultsWriterFunc) WriteResults

func (f ResultsWriterFunc) WriteResults(results []any, resultErr error, response http.ResponseWriter, request *http.Request) error

Jump to

Keyboard shortcuts

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