echomiddleware

package module
v1.0.3-responsibleapi.2 Latest Latest
Warning

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

Go to latest
Published: May 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README ΒΆ

OpenAPI Validation Middleware for labstack/echo servers

Middleware for the Echo web server to perform validation of incoming requests via an OpenAPI specification.

This project is a lightweight wrapper over the excellent kin-openapi library's openapi3filter package.

This is intended to be used with code that's generated through oapi-codegen, but should work otherwise.

⚠️ This README may be for the latest development version, which may contain unreleased changes. Please ensure you're looking at the README for the latest release version.

Usage

You can add the middleware to your project with:

go get github.com/responsibleapi/echo-middleware

There is a full example of usage in the Go doc for this project.

A simplified version of this code is as follows:

rawSpec := `
openapi: "3.0.0"
# ...
`
spec, _ := openapi3.NewLoader().LoadFromData([]byte(rawSpec))

// NOTE that we need to make sure that the `Servers` aren't set, otherwise the OpenAPI validation middleware will validate that the `Host` header (of incoming requests) are targeting known `Servers` in the OpenAPI spec
// See also: Options#SilenceServersWarning
spec.Servers = nil

e := echo.New()
e.POST("/resource", func(c *echo.Context) error {
    fmt.Printf("%s /resource was called\n", c.Request().Method)

    return c.NoContent(http.StatusNoContent)
})

// create middleware
mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{
    Options: openapi3filter.Options{
        AuthenticationFunc: authenticationFunc,
    },
})

e.Use(mw)

// now all HTTP routes will be handled by the middleware, and any requests that are invalid will be rejected

FAQs

What versions of Echo does this support?
Version Supported?
github.com/labstack/echo/v4 ❌
github.com/labstack/echo/v5 βœ…
"This doesn't support ..." / "I think it's a bug that ..."

As this project is a lightweight wrapper over kin-openapi's openapi3filter package, it's likely that any bugs/features are better sent upstream.

However, it's worth raising an issue here instead, as it'll allow us to triage it before it goes to the kin-openapi maintainers.

Additionally, as oapi-codegen contains a number of middleware modules, we'll very likely want to implement the same functionality across all the middlewares, so it may take a bit more coordination to get the changes in across our middlewares.

I've just updated my version of kin-openapi, and now I can't build my code 😠

The kin-openapi project - which we πŸ’œ for providing a great library and set of tooling for interacting with OpenAPI - is a pre-v1 release, which means that they're within their rights to push breaking changes.

This may lead to breakage in your consuming code, and if so, sorry that's happened!

We'll be aware of the issue, and will work to update both the core oapi-codegen and the middlewares accordingly.

Documentation ΒΆ

Overview ΒΆ

Provide HTTP middleware functionality to validate that incoming requests conform to a given OpenAPI 3.x specification.

This provides middleware for an echo/v5 HTTP server.

This package is a lightweight wrapper over https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3filter from https://pkg.go.dev/github.com/getkin/kin-openapi.

This is _intended_ to be used with code that's generated through https://pkg.go.dev/github.com/oapi-codegen/oapi-codegen, but should work otherwise.

Index ΒΆ

Examples ΒΆ

Constants ΒΆ

View Source
const (
	EchoContextKey = "oapi-codegen/echo-context"
	UserDataKey    = "oapi-codegen/user-data"
)

Variables ΒΆ

This section is empty.

Functions ΒΆ

func GetEchoContext ΒΆ

func GetEchoContext(c context.Context) *echo.Context

GetEchoContext gets the echo context from within requests. It returns nil if not found or wrong type.

func GetUserData ΒΆ

func GetUserData(c context.Context) any

func OapiRequestValidator ΒΆ

func OapiRequestValidator(spec *openapi3.T) echo.MiddlewareFunc

OapiRequestValidator Creates the middleware to validate that incoming requests match the given OpenAPI 3.x spec, with a default set of configuration.

func OapiRequestValidatorWithOptions ΒΆ

func OapiRequestValidatorWithOptions(spec *openapi3.T, options *Options) echo.MiddlewareFunc

OapiRequestValidatorWithOptions Creates the middleware to validate that incoming requests match the given OpenAPI 3.x spec, allowing explicit configuration.

NOTE that this may panic if the OpenAPI spec isn't valid, or if it cannot be used to create the middleware

Example ΒΆ
rawSpec := `
openapi: "3.0.0"
info:
  version: 1.0.0
  title: TestServer
servers:
  - url: http://example.com/
paths:
  /resource:
    post:
      operationId: createResource
      responses:
        '204':
          description: No content
      requestBody:
        required: true
        content:
          application/json:
            schema:
              properties:
                name:
                  type: string
              additionalProperties: false
  /protected_resource:
    get:
      operationId: getProtectedResource
      security:
        - BearerAuth:
            - someScope
      responses:
        '204':
          description: no content
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
`

must := func(err error) {
	if err != nil {
		panic(err)
	}
}

logResponseBody := func(rr *httptest.ResponseRecorder) {
	if rr.Result().Body != nil {
		data, _ := io.ReadAll(rr.Result().Body)
		if len(data) > 0 {
			fmt.Printf("Response body: %s", data)
		}
	}
}

spec, err := openapi3.NewLoader().LoadFromData([]byte(rawSpec))
must(err)

// NOTE that we need to make sure that the `Servers` aren't set, otherwise the OpenAPI validation middleware will validate that the `Host` header (of incoming requests) are targeting known `Servers` in the OpenAPI spec
// See also: Options#SilenceServersWarning
spec.Servers = nil

e := echo.New()
e.POST("/resource", func(c *echo.Context) error {
	fmt.Printf("%s /resource was called\n", c.Request().Method)

	return c.NoContent(http.StatusNoContent)
})

e.GET("/protected_resource", func(c *echo.Context) error {
	// NOTE that we're setting up our `authenticationFunc` (below) to /never/ allow any requests in - so if we get a response from this endpoint, our `authenticationFunc` hasn't correctly worked
	return c.NoContent(http.StatusNoContent)
})

authenticationFunc := func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
	fmt.Printf("`AuthenticationFunc` was called for securitySchemeName=%s\n", ai.SecuritySchemeName)
	return fmt.Errorf("this check always fails - don't let anyone in!")
}

// create middleware
mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{
	Options: openapi3filter.Options{
		AuthenticationFunc: authenticationFunc,
	},
})

e.Use(mw)

// ================================================================================
fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (with no request body)")

req, err := http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(nil))
must(err)
req.Header.Set("Content-Type", "application/json")

rr := httptest.NewRecorder()

e.ServeHTTP(rr, req)

fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code)
logResponseBody(rr)
fmt.Println()

// ================================================================================
fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (because an invalid property is sent, and we have `additionalProperties: false`)")
body := map[string]string{
	"invalid": "not expected",
}

data, err := json.Marshal(body)
must(err)

req, err = http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(data))
must(err)
req.Header.Set("Content-Type", "application/json")

rr = httptest.NewRecorder()

e.ServeHTTP(rr, req)

fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code)
logResponseBody(rr)
fmt.Println()

// ================================================================================
fmt.Println("# A request that uses the wrong HTTP method is rejected with HTTP 405 Method Not Allowed")
req, err = http.NewRequest(http.MethodDelete, "/resource", bytes.NewReader(data))
must(err)
req.Header.Set("Content-Type", "application/json")

rr = httptest.NewRecorder()

e.ServeHTTP(rr, req)

fmt.Printf("Received an HTTP %d response. Expected HTTP 405\n", rr.Code)
logResponseBody(rr)
fmt.Println()

// ================================================================================
fmt.Println("# A request that is well-formed is passed through to the Handler")
body = map[string]string{
	"name": "Jamie",
}

data, err = json.Marshal(body)
must(err)

req, err = http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(data))
must(err)
req.Header.Set("Content-Type", "application/json")

rr = httptest.NewRecorder()

e.ServeHTTP(rr, req)

fmt.Printf("Received an HTTP %d response. Expected HTTP 204\n", rr.Code)
logResponseBody(rr)
fmt.Println()

// ================================================================================
fmt.Println("# A request to an authenticated endpoint must go through an `AuthenticationFunc`, and if it fails, an HTTP 403 is returned")

req, err = http.NewRequest(http.MethodGet, "/protected_resource", nil)
must(err)

rr = httptest.NewRecorder()

e.ServeHTTP(rr, req)

fmt.Printf("Received an HTTP %d response. Expected HTTP 403\n", rr.Code)
logResponseBody(rr)
fmt.Println()
Output:
# A request that is malformed is rejected with HTTP 400 Bad Request (with no request body)
Received an HTTP 400 response. Expected HTTP 400
Response body: {"message":"request body has an error: value is required but missing"}

# A request that is malformed is rejected with HTTP 400 Bad Request (because an invalid property is sent, and we have `additionalProperties: false`)
Received an HTTP 400 response. Expected HTTP 400
Response body: {"message":"request body has an error: doesn't match schema: property \"invalid\" is unsupported"}

# A request that uses the wrong HTTP method is rejected with HTTP 405 Method Not Allowed
Received an HTTP 405 response. Expected HTTP 405
Response body: {"message":"Method Not Allowed"}

# A request that is well-formed is passed through to the Handler
POST /resource was called
Received an HTTP 204 response. Expected HTTP 204

# A request to an authenticated endpoint must go through an `AuthenticationFunc`, and if it fails, an HTTP 403 is returned
`AuthenticationFunc` was called for securitySchemeName=BearerAuth
Received an HTTP 403 response. Expected HTTP 403
Response body: {"message":"security requirements failed: this check always fails - don't let anyone in!"}
Example (WithErrorHandler) ΒΆ
rawSpec := `
openapi: "3.0.0"
info:
  version: 1.0.0
  title: TestServer
servers:
  - url: http://example.com/
paths:
  /resource:
    post:
      operationId: createResource
      responses:
        '204':
          description: No content
      requestBody:
        required: true
        content:
          application/json:
            schema:
              properties:
                name:
                  type: string
              additionalProperties: false
`

must := func(err error) {
	if err != nil {
		panic(err)
	}
}

logResponseBody := func(rr *httptest.ResponseRecorder) {
	if rr.Result().Body != nil {
		data, _ := io.ReadAll(rr.Result().Body)
		if len(data) > 0 {
			fmt.Printf("Response body: %s", data)
		}
	}
}

spec, err := openapi3.NewLoader().LoadFromData([]byte(rawSpec))
must(err)

// NOTE that we need to make sure that the `Servers` aren't set, otherwise the OpenAPI validation middleware will validate that the `Host` header (of incoming requests) are targeting known `Servers` in the OpenAPI spec
// See also: Options#SilenceServersWarning
spec.Servers = nil

e := echo.New()
e.POST("/resource", func(c *echo.Context) error {
	fmt.Printf("%s /resource was called\n", c.Request().Method)

	return c.NoContent(http.StatusNoContent)
})

authenticationFunc := func(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
	fmt.Printf("`AuthenticationFunc` was called for securitySchemeName=%s\n", ai.SecuritySchemeName)
	return fmt.Errorf("this check always fails - don't let anyone in!")
}

errorHandlerFunc := func(c *echo.Context, err *echo.HTTPError) error {
	fmt.Printf("ErrorHandler: An HTTP %d was returned by the middleware with error message: %s\n", err.Code, err.Message)
	return c.String(err.Code, "This was rewritten by the ErrorHandler")
}

// create middleware
mw := middleware.OapiRequestValidatorWithOptions(spec, &middleware.Options{
	Options: openapi3filter.Options{
		AuthenticationFunc: authenticationFunc,
	},
	ErrorHandler: errorHandlerFunc,
})

// then wire it in
e.Use(mw)

// ================================================================================
fmt.Println("# A request that is malformed is rejected with HTTP 400 Bad Request (with no request body), and is then logged by the ErrorHandler")

req, err := http.NewRequest(http.MethodPost, "/resource", bytes.NewReader(nil))
must(err)
req.Header.Set("Content-Type", "application/json")

rr := httptest.NewRecorder()

e.ServeHTTP(rr, req)

fmt.Printf("Received an HTTP %d response. Expected HTTP 400\n", rr.Code)
logResponseBody(rr)
Output:
# A request that is malformed is rejected with HTTP 400 Bad Request (with no request body), and is then logged by the ErrorHandler
ErrorHandler: An HTTP 400 was returned by the middleware with error message: request body has an error: value is required but missing
Received an HTTP 400 response. Expected HTTP 400
Response body: This was rewritten by the ErrorHandler

func OapiValidatorFromYamlFile ΒΆ

func OapiValidatorFromYamlFile(path string) (echo.MiddlewareFunc, error)

OapiValidatorFromYamlFile is an Echo middleware function which validates incoming HTTP requests to make sure that they conform to the given OAPI 3.0 specification. When OAPI validation fails on the request, we return an HTTP/400. Create validator middleware from a YAML file path

func ValidateRequestFromContext ΒΆ

func ValidateRequestFromContext(ctx *echo.Context, router routers.Router, options *Options) *echo.HTTPError

ValidateRequestFromContext is called from the middleware above and actually does the work of validating a request.

Types ΒΆ

type ErrorHandler ΒΆ

type ErrorHandler func(c *echo.Context, err *echo.HTTPError) error

ErrorHandler is called when there is an error in validation

type MultiErrorHandler ΒΆ

type MultiErrorHandler func(openapi3.MultiError) *echo.HTTPError

MultiErrorHandler is called when the OpenAPI filter returns an openapi3.MultiError (https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3#MultiError)

type Options ΒΆ

type Options struct {
	// ErrorHandler is called when a validation error occurs.
	//
	// If not provided, `http.Error` will be called
	ErrorHandler ErrorHandler
	// Options contains any configuration for the underlying `openapi3filter`
	Options openapi3filter.Options
	// ParamDecoder is the openapi3filter.ContentParameterDecoder to be used for the decoding of the request body
	//
	// If unset, a default will be used
	ParamDecoder openapi3filter.ContentParameterDecoder
	// UserData is any user-specified data to inject into the context.Context, which is then passed in to the validation function.
	//
	// Set on the Context with the key `UserDataKey`.
	UserData any
	// Skipper an echo Skipper to allow skipping the middleware.
	Skipper echomiddleware.Skipper
	// MultiErrorHandler is called when there is an openapi3.MultiError (https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3#MultiError) returned by the `openapi3filter`.
	//
	// If not provided `defaultMultiErrorHandler` will be used.
	MultiErrorHandler MultiErrorHandler
	// SilenceServersWarning allows silencing a warning for https://github.com/oapi-codegen/oapi-codegen/issues/882 that reports when an OpenAPI spec has `spec.Servers != nil`
	SilenceServersWarning bool
	// DoNotValidateServers ensures that there is no Host validation performed (see `SilenceServersWarning` and https://github.com/deepmap/oapi-codegen/issues/882 for more details)
	DoNotValidateServers bool
	// Prefix is stripped from the request path before validation. This is useful when the API is mounted under a sub-path
	// (e.g. "/api") that isn't part of the OpenAPI spec's paths. The prefix must start with "/" if set.
	Prefix string
}

Options to customize request validation. These are passed through to openapi3filter.

Jump to

Keyboard shortcuts

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