httpie

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Mar 1, 2024 License: MIT Imports: 14 Imported by: 0

README

Build and Test Coverage Status Docs License Version

Opinionated middleware, and helper functions for HTTP based applications.

Why httpie? So you can have your pie and eat it too...?

Logo

Middleware

TransactionalMiddleware

The transaction middleware will embed a transaction (sql.Tx) into your context.

You must provide a function which provides the sql.Tx to the middleware:

middleware := httpie.TransactionalMiddleware(func(ctx context.Context) (*sql.Tx, error) {
  return db.BeginTx(ctx, nil)
})

You can then access the transaction from the context:

func getTx(ctx context.Context) *sql.Tx {
  tx := context.Value(httpie.TransactionCtxKey)
  if tx != nil {
    return tx.(*sql.Tx)
  }
  return nil
}

Note: If a transaction is not present then your repository / service layer should either acquire one itself, or not use a transaction and rely on your normal DB.Query style calls.

The transaction will only be created when the HTTP request is: PUT, POST, DELETE

The transaction will be rolled back if the HTTP status is >= 400

The transaction will be automatically comitted if the HTTP status is < 400

Logging Middleware

The logging middleware will use slog to record requests and responses.

You need to provide it with a slog.Logger and any configuration. The default logs a lot of common values.

middleware := httpie.LoggingMiddleware(slog.Default(), httpie.LoggingOpts{
  LogRequest: true,
  LogResponse: true,
  OnResponse: httpie.DefaultLogResponse,
  OnRequest: httpie.DefaultLogRequest,
})

You can customize the response and request logging by providing your own OnResponse and OnRequest handlers.

Helpers

There are various other helpers for reading/writing JSON and handling errors.

WriteErr(w http.ResponseWriter, err error) error
WriteOk(w http.ResponseWriter, data T) error
WriteOkOrErr(w http.ResponseWriter, data T, err error)
ReadJson(r *http.Request, data *T) error
GetQueryParamIntDefault(r *http.Request, key string, defaultValue int) (int, error)
GetQueryParamListDefault(r *http.Request, key string, defaultValue []string) ([]string, error)
GetQueryParamDefault(r *http.Request, key string, defaultValue string) (string, error)

Validation

There is a slightly modified version of github.com/go-playground/validator/v10 that has a secure password validator (securepassword)

You can use this validator as follows:

type MyStruct struct {
  password string `validate:"required,securepassword"`
}

You can execute the validator by running:

err := httpie.Validate(MyStruct{password: "test"})

err will be an ErrHttpValidation if validation fails. See Validation Error

Errors

There is a non standard error system in place that is useful for mapping common errors that may occur in the repository or service layer.

These errors are below and will map to standard HTTP errors when using WriteErr. Any other error passed to WriteErr will trigger a 500 internal service error.

Error Name Status Code Message Purpose
ErrBadRequest 400 Bad Request Used to indicate the request was malformed
ErrUnauthorized 401 Unauthorized Used to indicate the request has missing or invalid authorization
ErrForbidden 403 Forbidden The user is authenticated but not authorized for the resource
ErrNotFound 404 Not Found The resource was not found
ErrConflict 409 Conflict The resource already exists
ErrInternal 500 Internal Server Error There was an unexepected error

These errors are not meant to be comprehensive, it is useful to have errors that may occur in the service layer (like not finding an object) be able to propagate with the correct http error codes.

You can implement new error codes like this:

var ErrMyError = httpie.NewErrHttp(status_code, "error_message")

The errors when rendered using WriteErr will be in JSON format:

{
  "message": "not found"
}

Validation Errors

There is a special variant of ErrHttp called ErrHttpValidation. This includes some extra information for returning a map[string]string of errors.

These errors are meant for an API which understands the failures.

You can use it as follows:

err := NewHttpErrValidation(map[string]string{"field":"error_code"})

When passed to WriteErr it will return a 400 bad request with the following JSON output:

{
  "message": "validation failed",
  "errors": {
    "field": "error_code"
  }
}

Watched Response Writer

In middleware you often want to be able to look at the response, and optionally override it before actually writing it to the client.

There is a WatchedResponseWriter that is a simple wrapper around http.ResponseWriter

It will delay actually writing any requests to the response until Apply() is called. It can also be Reset() if the middleware determines it want's to send something else.

This is used by the TransactionalMiddleware to ensure we send an internal server error if a tx.Commit() fails.

It is also used by the LoggingMiddleware to capture the HTTP status code.

Note: This naively uses a buffer to capture the written bytes, it's likely not a problem but for something high performance this could be an issue [just a theory]

You can use it in middleware like this:

// Middleware that will convert any http status code >=400 into a 500 internal server error
func MyMiddlewareHandler(w http.ResponseWriter, r *http.Request) {
  // Create a watched response writer
  ww := httpie.NewWatchedResponseWriter(w)
  
  // We need to apply the response when we are ready
  defer ww.Apply()
  
  // Call the http handler
  next.ServeHTTP(ww, r)

  // Detect some error and do something different
  if ww.StatusCode() >= 400 {
    // Reset the response buffer and status code
    ww.Reset()

    // Send a totally different message
    ww.WriteHeader(500)
    ww.Write("internal server error")
    // OR httpie.WriteErr(ww, ErrInternal)
  }
}

Context

The middleware in this package tends to inject things into the context. You often need to be able to pull things out of the context.

GetContextValue

You can get a typed object out of the context as follows:

type myKeyType int
var uniqueCtxKey myKeyType = 0

type MyStruct struct {
  MyValue int
}

myStruct := MyStruct {
  MyValue: 42,
}

// Assign to the context
ctx := context.WithValue(context.Background(), uniqueCtxKey, &myStruct)

// Get from the context
ctx = httpie.GetContextValue[MyStruct](ctx, ctxKey)

The value will be nil if it did not exist in the context or if the type was incorrect.

Clock Service

The clock service is a simple wrapper around time.Now().UTC(). It's purpose is to allow fine-grained mocking in your service layer by including the clock service as a dependency.

You can make use of ClockServiceMock to mock time in your service layer during testing.

To use the clock service:

type MyService struct {
  clockService IClockService
}

func (s *MyService) GetNow() time.Time {
  return s.clockService.Now()
}

func NewMyService(clockService IClockService) *MyService {
  return &MyService{
    clockService,
  }
}

cs := new(ClockService)
myService := NewMyService(clockService)
myService.GetNow()

Documentation

Index

Constants

View Source
const MIN_ENTROPY = 60

Variables

View Source
var (
	ErrNotFound     = NewErrHttp(http.StatusNotFound, "not found")
	ErrUnauthorized = NewErrHttp(http.StatusUnauthorized, "unauthorized")
	ErrBadRequest   = NewErrHttp(http.StatusBadRequest, "bad request")
	ErrForbidden    = NewErrHttp(http.StatusForbidden, "forbidden")
	ErrConflict     = NewErrHttp(http.StatusConflict, "conflict")
	ErrInternal     = NewErrHttp(http.StatusInternalServerError, "internal server error")
)

These are standard errors that should be returned at the repository level, its not meant to be exhaustive of all HTTP errors but rather standard ones that make sense to propagate up from the services and repositories.

View Source
var DefaultLoggingOpts = LoggingOpts{
	LogRequest:  true,
	LogResponse: true,
	OnRequest:   DefaultLogRequest,
	OnResponse:  DefaultLogResponse,
}
View Source
var TransactionCtxKey ctxKey = 0
View Source
var Validator *validator.Validate

Functions

func DefaultLogRequest

func DefaultLogRequest(ctx context.Context, slogger *slog.Logger, r *http.Request, start time.Time)

func DefaultLogResponse

func DefaultLogResponse(ctx context.Context, slogger *slog.Logger, r *http.Request, ww *WatchedResponseWriter, start time.Time)

func GetContextValue

func GetContextValue[T any](ctx context.Context, key any) *T

func GetQueryParam

func GetQueryParam(r *http.Request, key string) (bool, string)

Get a query parameter (string) from the request, return a boolean if it exists

func GetQueryParamDefault

func GetQueryParamDefault(r *http.Request, key string, defaultValue string) string

Get a query parameter (string) from the request, use a default value if it does not exist

func GetQueryParamInt

func GetQueryParamInt(r *http.Request, key string) (bool, int, error)

Get a query parameter (int) from the request, return a boolean if it exists, and an error if its invalid

func GetQueryParamIntDefault

func GetQueryParamIntDefault(r *http.Request, key string, defaultValue int) (int, error)

Get a query parameter (int) from the request, use a default value if it does not exist and an error if its invalid

func GetQueryParamList

func GetQueryParamList(r *http.Request, key string) (bool, []string)

Get a query parameter ([]string) from the request, split the value by commas, return a boolean if it exists

func GetQueryParamListDefault

func GetQueryParamListDefault(r *http.Request, key string, defaultValue []string) []string

Get a query parameter ([]string) from the request, split the value by commas, use a default value if it does not exist

func LoggingMiddleware

func LoggingMiddleware(slogger *slog.Logger, opts ...LoggingOpts) func(http.Handler) http.Handler

LoggingMiddleware logs the request and response of an http handler to a slog.Logger

func ReadJson

func ReadJson[T any](r *http.Request, data *T) error

Read a JSON document from the request body and unmarshal it into the provided data object

func SecurePasswordValidator

func SecurePasswordValidator(fl validator.FieldLevel) bool

Custom validator for secure passwords - ensures entropy is greater than 60

func TransactionalMiddleware

func TransactionalMiddleware(getTx func(ctx context.Context) (driver.Tx, error)) func(http.Handler) http.Handler

TransactionMiddleware injects a transaction into the request context and handles the commit/rollback

func WriteAccepted

func WriteAccepted(w http.ResponseWriter) error

Used for successful operations that dont return a body

func WriteErr

func WriteErr(w http.ResponseWriter, err error) error

Used for operations that resulted in a failure, returns a JSON error Determines the status code from the error if possible, defaults to 500

func WriteErrJson

func WriteErrJson(w http.ResponseWriter, status int, message string) error

Used for operations that resulted in a failure, returns a JSON error with the specified status code

func WriteErrValidationJson

func WriteErrValidationJson(w http.ResponseWriter, validationErr IErrHttpValidation) error

Used for operations that resulted in a failure, returns a JSON error with the specified status code and validation errors

func WriteOk

func WriteOk[T any](w http.ResponseWriter, data T) error

Used for successful operations that return a body

func WriteOkOrErr

func WriteOkOrErr[T any](w http.ResponseWriter, data T, err error)

Helper to write either an error or a successful response

Types

type ClockService

type ClockService struct{}

func (*ClockService) Now

func (c *ClockService) Now() time.Time

type ClockServiceMock

type ClockServiceMock struct {
	mock.Mock
}

func (*ClockServiceMock) Now

func (c *ClockServiceMock) Now() time.Time

type ErrHttp

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

func NewErrHttp

func NewErrHttp(statusCode int, error string) ErrHttp

func (ErrHttp) Error

func (e ErrHttp) Error() string

func (ErrHttp) StatusCode

func (e ErrHttp) StatusCode() int

type ErrHttpValidation

type ErrHttpValidation struct {
	ErrHttp
	// contains filtered or unexported fields
}

func NewErrHttpValidation

func NewErrHttpValidation(errors map[string]string) ErrHttpValidation

func (ErrHttpValidation) ValidationErrors

func (e ErrHttpValidation) ValidationErrors() map[string]string

type IClockService

type IClockService interface {
	Now() time.Time
}

type IErrHttp

type IErrHttp interface {
	StatusCode() int
	Error() string
}

type IErrHttpValidation

type IErrHttpValidation interface {
	StatusCode() int
	Error() string
	ValidationErrors() map[string]string
}

func Validate

func Validate(s any) IErrHttpValidation

Validate an object and return a validation error if any

type LoggingOpts

type LoggingOpts struct {
	LogRequest  bool
	LogResponse bool
	OnRequest   func(ctx context.Context, slogger *slog.Logger, r *http.Request, start time.Time)
	OnResponse  func(ctx context.Context, slogger *slog.Logger, r *http.Request, ww *WatchedResponseWriter, start time.Time)
}

type WatchedResponseWriter

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

Wraps an http.ResponseWriter and watches for changes to the response

func NewWatchedResponseWriter

func NewWatchedResponseWriter(response http.ResponseWriter) *WatchedResponseWriter

Create a new WatchedResponseWriter

func (*WatchedResponseWriter) Apply

func (w *WatchedResponseWriter) Apply()

Apply the captured status code and bytes to the wrapped response

func (*WatchedResponseWriter) BytesWritten

func (w *WatchedResponseWriter) BytesWritten() int

Return the number of bytes written

func (*WatchedResponseWriter) Header

func (w *WatchedResponseWriter) Header() http.Header

Delegate the Header method to the wrapped response

func (*WatchedResponseWriter) Reset

func (w *WatchedResponseWriter) Reset()

Reset the status code, bytes written, and buffer

func (*WatchedResponseWriter) StatusCode

func (w *WatchedResponseWriter) StatusCode() int

Return the captured status code

func (*WatchedResponseWriter) Write

func (w *WatchedResponseWriter) Write(b []byte) (int, error)

Capture the written bytes to a buffer

func (*WatchedResponseWriter) WriteHeader

func (w *WatchedResponseWriter) WriteHeader(statusCode int)

Capture the written status code

Jump to

Keyboard shortcuts

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