Oaskit.Plugs.ValidateRequest (oaskit v0.2.0)

View Source

This plug will match incoming requests to operations defined with the Oaskit.Controller.operation/2 or Oaskit.Controller.use_operation/2 macros and retrieved from a provided OpenAPI Specification module.

Pluggin' in

To use this plug in a controller, the Oaskit.Plugs.SpecProvider plug must be used in the router for the corresponding routes.

defmodule MyAppWeb.Router do
  use Phoenix.Router

  pipeline :api do
    plug Oaskit.Plugs.SpecProvider, spec: MyAppWeb.OpenAPISpec
  end

  scope "/api" do
    pipe_through :api

    scope "/users", MyAppWeb do
      get "/", UserController, :list_users
    end
  end
end

This plug must then be used in controllers. It is possible to call the plug in every controller where you want validation, or to define it globally in your MyAppWeb module.

While you can directly patch the MyAppWeb.controller function if all your controllers belong to the HTTP API, we suggest to create a new api_controller function in your MyAppWeb module.

Duplicate the controller function and add this plug and also use Oaskit.Controller.

defmodule MyAppWeb do
  def controller do
    # ...
  end

  def api_controller do
    quote do
      use Phoenix.Controller, formats: [:json], layouts: []

      # Use the controller helpers to define operations
      use Oaskit.Controller

      use Gettext, backend: MyAppWeb.Gettext
      import Plug.Conn

      # Use the plug here. This has to be after `use Phoenix.Controller`.
      plug Oaskit.Plugs.ValidateRequest

      unquote(verified_routes())
    end
  end
end

Finally, request body validation will only work if the body is fetched from the conn. This is generally done by the Plug.Parsers plug. It can also be done by a custom plug if you are implementing an API that is working with plaintext or custom formats.

Request validation

Requests will be validated according to the request body schemas and parameter schemas defined in operations. The data will also be cast to the expected types:

  • Parameters whose schemas define a type of boolean or integer will be cast to that type. Arrays of such types are supported as well.
  • Parameters with type string and a format supported by JSV will be also cast according to that format. Output values for formats are described in the JSV documentation. This includes, URI, Date, DateTime and Duration.
  • Request bodies are cast to their given schema too. When using raw schemas defined as maps, the main changes to the data is the cast of formats, just as for parameters. When schemas are defined using module names, and when those modules' structs are created with JSV.defschema/1, the request bodies will be cast to those structs.

Request bodies will be validated according to the expected operation content-types, and the actual content-type of the request.

Options

  • :query_reader_opts - If a Plug.Conn struct enters this plug without its query parameters being fetched, this plug will fetch them automatically using Conn.fetch_query_params(conn, query_reader_opts). The default value is [length: 1_000_000, validate_utf8: true].
  • :error_handler - A module or {module, argument} tuple. The error handler must implement the Oaskit.ErrorHandler behaviour. It will be called on validation errors. Defaults to Oaskit.ErrorHandler.Default.
  • :pretty_errors - A boolean to control pretty printing of JSON errors payload in error handlers. Defaults to true when Mix.env() != :prod, defaults to false otherwise.
  • :html_errors - A boolean to control whether the default error handler is allowed to return HTML errors when the request accepts HTML. This is useful to quickly read errors when opening an url directly from the browser. Defaults to true.
  • Unknown options are collected and passed to the error handler.

Non-required bodies

A request body is considered empty if "", nil or an empty map (%{}). In that case, if the operation defines the request body with required: false (which is the default value!), the body validation will be skipped.

The empty map is a special case because Plug.Parsers implementations cannot return anything else than a map. If a client sends an HTTP request with an "application/json" content-type but no body, the JSON parser in the Plug library will still return an empty map.

To avoid problems, always define request bodies as required if you can. This is made automatically when using the "definition shortcuts" described in Oaskit.Controller.operation/2.

Error handling

Validation can fail at various steps:

  • Parameters validation
  • Content-type matching
  • Body validation

On failure, the validation stops immediately. If a parameter is invalid, the body is not validated and the error handler is called with a single category of errors. In the case of parameters, multiple errors can be passed to the handler if multiple parameters are invalid or missing.

Custom error handlers must implement the Oaskit.ErrorHandler behaviour and be ready to accept all error reasons that this plug can generate. Such reasons are described in the Oaskit.ErrorHandler.reason/0 type.

The 3rd argment passed to the Oaskit.ErrorHandler.handle_error/3 depends on the :error_handler function. When defined as a module, the argument is all the other options given to this plug:

plug Oaskit.Plugs.ValidateRequest,
  error_handler: MyErrorHandler,
  pretty_errors: true,
  custom_opt: "foo"

This will allow the handler to receive :pretty_errors, :custom_opt and other options with default values.

When passing a tuple, the second element will be passed as-is:

plug Oaskit.Plugs.ValidateRequest,
  error_handler: {MyErrorHandler, "foo"}

In the example above, the error handler will only receive "foo" as the 3rd argument of Oaskit.ErrorHandler.handle_error/3.