httpx

package
v0.0.0-...-c5cf874 Latest Latest
Warning

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

Go to latest
Published: May 20, 2024 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package httpx provides additional [http.Handler]s and utility functions

Index

Examples

Constants

View Source
const (
	ErrInternalServerError = StatusCode(http.StatusInternalServerError)
	ErrBadRequest          = StatusCode(http.StatusBadRequest)
	ErrNotFound            = StatusCode(http.StatusNotFound)
	ErrForbidden           = StatusCode(http.StatusForbidden)
	ErrMethodNotAllowed    = StatusCode(http.StatusMethodNotAllowed)
)

Common Errors accepted by most httpx functions.

These are guaranteed to implement both [error] and http.Handler. See also [StatusCodeError].

View Source
const (
	ContentTypeText = "text/plain; charset=utf-8"
	ContentTypeHTML = "text/html; charset=utf-8"
	ContentTypeJSON = "application/json; charset=utf-8"
)

Content Types for standard content offered by several functions.

Variables

This section is empty.

Functions

func RenderErrorPage

func RenderErrorPage(err error, res Response, w http.ResponseWriter, r *http.Request)

RenderErrorPage renders a debug error page instead of the fallback response res. The error page is intended to replace error pages for debugging and should not be used in production.

It will keep the original status code of res, but will replace the content type and body. It will render as 'text/html'.

Example

Render an error page in response to a panic

package main

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

	_ "embed"
	"github.com/tkw1536/pkglib/httpx"
)

func main() {

	// response for errors
	res := httpx.Response{StatusCode: http.StatusNotFound, Body: []byte("not found")}

	// a handler with an error page
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// do some operation, here it always fails for testing.
		err := fmt.Errorf("for debugging: %w", httpx.ErrNotFound)
		if err != nil {
			// render the error page
			httpx.RenderErrorPage(err, res, w, r)
			return
		}

		// ... do some normal processing here ...
		panic("normal rendering, never reached")
	})

	// run the request
	{
		req, err := http.NewRequest(http.MethodGet, "/", nil)
		if err != nil {
			panic(err)
		}
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)

		result := rr.Result()

		fmt.Printf("Got status: %d\n", result.StatusCode)
		fmt.Printf("Got content-type: %s\n", result.Header.Get("Content-Type"))
	}

}
Output:

Got status: 404
Got content-type: text/html; charset=utf-8
Example (Panic)

Render an error page in response to a panic

package main

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

	_ "embed"
	"github.com/tkw1536/pkglib/httpx"
	"github.com/tkw1536/pkglib/recovery"
)

func main() {

	// response for errors
	res := httpx.Response{StatusCode: http.StatusInternalServerError, Body: []byte("something went wrong")}

	// a handler with an error page
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			// recover any errors
			if err := recovery.Recover(recover()); err != nil {
				// render the error page
				// which will replace it with a text/html page
				httpx.RenderErrorPage(err, res, w, r)
			}
		}()

		// ... do actual code ...
		panic("error for testing")
	})

	// run the request
	{
		req, err := http.NewRequest(http.MethodGet, "/", nil)
		if err != nil {
			panic(err)
		}
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)

		result := rr.Result()

		fmt.Printf("Got status: %d\n", result.StatusCode)
		fmt.Printf("Got content-type: %s\n", result.Header.Get("Content-Type"))
	}

}
Output:

Got status: 500
Got content-type: text/html; charset=utf-8

Types

type ErrInterceptor

type ErrInterceptor struct {
	// Errors determines which error classes are intercepted.
	//
	// Errors are compared using [errors.Is] is undetermined order.
	// This means that if an error that [errors.Is] for multiple keys,
	// the returned response may any of the values.
	Errors map[error]Response

	// Fallback is the response for errors that are not of any of the above error classes.
	Fallback Response

	// RenderError indicates that instead of intercepting an error regularly
	// a human-readable error page with the appropriate error code should be displayed.
	// See [ErrorPage] for documentation on the error page handler.
	//
	// This option should only be used during development, as it exposes potentially security-critical data.
	RenderError bool

	// OnFallback is called when an unknown error is intercepted.
	OnFallback func(*http.Request, error)
}

ErrInterceptor can handle errors for http responses and render appropriate error responses.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/tkw1536/pkglib/httpx"
)

func main() {
	// create an error interceptor
	interceptor := httpx.ErrInterceptor{

		// handle [ErrNotFound] with a not found response
		Errors: map[error]httpx.Response{
			// error not found (and wraps thereof) return that status code
			httpx.ErrNotFound: {
				StatusCode: http.StatusNotFound,
				Body:       []byte("Not Found"),
			},

			// forbidden (isn't actually used in this example)
			httpx.ErrForbidden: {
				StatusCode: http.StatusForbidden,
				Body:       []byte("Forbidden"),
			},
		},

		// fallback to a generic not found error
		Fallback: httpx.Response{
			StatusCode: http.StatusServiceUnavailable,
			Body:       []byte("fallback error"),
		},
	}

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// ... do some work ...
		// in prod this would be an error returned from some operation
		result := map[string]error{
			"/":         nil, // no error
			"/notfound": httpx.ErrNotFound,
			"/wrapped":  fmt.Errorf("wrapped: %w", httpx.ErrNotFound),
		}[r.URL.Path]

		// intercept an error
		if interceptor.Intercept(w, r, result) {
			return
		}

		_, _ = w.Write([]byte("Normal response"))
	})

	// a function to make a request to a specific method
	makeRequest := func(path string) {
		req, err := http.NewRequest(http.MethodGet, path, nil)
		if err != nil {
			panic(err)
		}

		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)

		rrr := rr.Result()
		result, _ := io.ReadAll(rrr.Body)
		fmt.Printf("%q returned code %d with %s %q\n", path, rrr.StatusCode, rrr.Header.Get("Content-Type"), string(result))
	}

	makeRequest("/")
	makeRequest("/notfound")
	makeRequest("/wrapped")

}
Output:

"/" returned code 200 with text/plain; charset=utf-8 "Normal response"
"/notfound" returned code 404 with text/plain; charset=utf-8 "Not Found"
"/wrapped" returned code 404 with text/plain; charset=utf-8 "Not Found"
var (
	TextInterceptor ErrInterceptor
	JSONInterceptor ErrInterceptor
	HTMLInterceptor ErrInterceptor
)

Common interceptors for specific content types.

These handle all common http status codes by sending their response with a common error code. See the Error constants of this package for supported errors.

func (ErrInterceptor) Intercept

func (ei ErrInterceptor) Intercept(w http.ResponseWriter, r *http.Request, err error) (intercepted bool)

Intercept intercepts the given error, and writes the response to the struct. A response is written to w if and only error is not nil. The return value indicates if error was nil and a response was written.

A typical use of an Intercept should be as follows:

// get interceptor from somewhere
var ei ErrInterceptor
// perform an operation, intercept the error or bail out
result, err := SomeOperation()
if ei.Intercept(w, r, err) {
	return
}

// ... write result to the response ...

The precise behavior of Intercept is documented inside ErrInterceptor itself.

type ErrorLogger

type ErrorLogger func(r *http.Request, err error)

ErrorLogger is a function that can log an error occurred during some http handling process. A nil logger performs no logging.

type Response

type Response struct {
	ContentType string // defaults to [ContentTypeTextPlain]
	Body        []byte // immutable body to be sent to the client

	Modtime    time.Time
	StatusCode int // defaults to a 2XX status code
}

Response represents a static http Response. It implements http.Handler.

Example

Using a response with a plain http status

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/tkw1536/pkglib/httpx"
)

func main() {
	response := httpx.Response{
		StatusCode:  http.StatusOK,
		ContentType: httpx.ContentTypeHTML,
		Body:        []byte("<!DOCTYPE html>Hello world"),
	}

	req, err := http.NewRequest(http.MethodGet, "/", nil)
	if err != nil {
		panic(err)
	}
	rr := httptest.NewRecorder()
	response.ServeHTTP(rr, req)

	result := rr.Result()
	body, _ := io.ReadAll(result.Body)

	fmt.Printf("Got status: %d\n", result.StatusCode)
	fmt.Printf("Got content-type: %s\n", result.Header.Get("Content-Type"))
	fmt.Printf("Got body: %s", string(body))

}
Output:

Got status: 200
Got content-type: text/html; charset=utf-8
Got body: <!DOCTYPE html>Hello world
Example (Defaults)

It is possible to omit everything, and defaults will be set correctly.

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/tkw1536/pkglib/httpx"
)

func main() {
	response := httpx.Response{
		Body: []byte("Hello world"),
	}

	req, err := http.NewRequest(http.MethodGet, "/", nil)
	if err != nil {
		panic(err)
	}
	rr := httptest.NewRecorder()
	response.ServeHTTP(rr, req)

	result := rr.Result()
	body, _ := io.ReadAll(result.Body)

	fmt.Printf("Got status: %d\n", result.StatusCode)
	fmt.Printf("Got content-type: %s\n", result.Header.Get("Content-Type"))
	fmt.Printf("Got body: %s", string(body))

}
Output:

Got status: 200
Got content-type: text/plain; charset=utf-8
Got body: Hello world

func (Response) Minify

func (response Response) Minify() Response

Minify returns a copy of the response with minified content.

func (Response) Now

func (response Response) Now() Response

Now returns a copy of the response with the Modtime field set to the current time in UTC.

Example

Using now one can set the time when the response was modified. This means that appropriate 'if-modified-since' headers are respected

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/tkw1536/pkglib/httpx"
)

func main() {
	response := httpx.Response{
		Body: []byte("Hello world"),
	}.Now()

	req, err := http.NewRequest(http.MethodGet, "/", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("If-Modified-Since", response.Modtime.Add(time.Second).Format(http.TimeFormat)) // set an if-modified-since

	rr := httptest.NewRecorder()
	response.ServeHTTP(rr, req)

	result := rr.Result()
	fmt.Printf("Got status: %d\n", result.StatusCode)

}
Output:

Got status: 304

func (Response) ServeHTTP

func (response Response) ServeHTTP(w http.ResponseWriter, r *http.Request)

type StatusCode

type StatusCode int

StatusCode represents an error based on a http status code. The integer is the http status code.

StatusCode implements both [error] and http.Handler. When used as a handler, it sets the appropriate status code, and returns a simple text response.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/tkw1536/pkglib/httpx"
)

func main() {
	// we can create an error based on the status code
	handler := httpx.StatusCode(http.StatusNotFound)

	// which automatically generates error messages
	fmt.Printf("String: %s\n", handler.String())
	fmt.Printf("GoString: %s\n", handler.GoString())
	fmt.Printf("Error: %s\n", handler.Error())

	// it also implements a static http.Handler
	{
		req, err := http.NewRequest(http.MethodGet, "/", nil)
		if err != nil {
			panic(err)
		}
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)

		rrr := rr.Result()
		body, _ := io.ReadAll(rrr.Body)

		fmt.Printf("ServeHTTP() status: %d\n", rrr.StatusCode)
		fmt.Printf("ServeHTTP() content-type: %s\n", rrr.Header.Get("Content-Type"))
		fmt.Printf("ServeHTTP() body: %s\n", body)
	}

}
Output:

String: Not Found
GoString: httpx.StatusCode(404/* Not Found */)
Error: httpx: Not Found
ServeHTTP() status: 404
ServeHTTP() content-type: text/plain; charset=utf-8
ServeHTTP() body: Not Found

func (StatusCode) Error

func (code StatusCode) Error() string

Error implements the built-in [error] interface.

func (StatusCode) GoString

func (code StatusCode) GoString() string

GoString returns a go source code representation of string.

func (StatusCode) ServeHTTP

func (code StatusCode) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

func (StatusCode) String

func (code StatusCode) String() string

String returns the status text belonging to this error.

Directories

Path Synopsis
_examples
Package content provides handlers for common http server content.
Package content provides handlers for common http server content.
Package form provides a form abstraction for http
Package form provides a form abstraction for http
Package mux provides [Mux]
Package mux provides [Mux]
Package wrap provides wrappers for [http.Handler]s.
Package wrap provides wrappers for [http.Handler]s.

Jump to

Keyboard shortcuts

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