@http/interceptor@0.27.0Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Functions to wrap HTTP handlers and intercept Requests and/or Responses. CORS, Logging, etc. Like middleware, but more outerwear.
Request/Response Interceptors
Similar to middleware, but more outerwear.
What is Middleware?
Middleware in most HTTP routers is form of
aspect-oriented programming,
but it's generally limited to the around advice. A middleware function is
given a next() function, that calls the next middleware or eventually the
request handler itself, but it may perform any logic it wants around this
call.
What are Interceptors?
Interceptors also take a leaf out of AOP, but instead let you define before and after advice.
Specifically:
- before the request handler - allowing interception of the
Request, and - after the handler - allowing interception of the
Response
Interceptors are registered using the intercept() function that wraps the
handler, which is given the set of interceptors, returning a new handler
function with all the interception baked in.
Here's an example of a Request Interceptor that ensures an
Authorization
header is present...
Deno.serve( withFallback( intercept( // This is the main handler... () => new Response("Hello world"), { // This is a RequestInterceptor that requires the Request to have an // Authorization header otherwise responds with a `401 Unauthorized`, // and asks for credentials. request: (req) => { if (!req.headers.has("Authorization")) { return unauthorized(`Basic realm="Who are you?"`); } }, }, ), ), );
Off the Shelf Interceptors
This package contains a number of pre-built interceptors...
- catchResponse - an error interceptor that catches any thrown Responses and return it as the actual Response
- cors - interceptors to handle CORS (Cross-Origin Resource Sharing) requests
- logging - interceptors to log Request info and Response status to the console
- skip - intercept Responses
by status and convert to a 'skipped' (
null) response, so that the request can then be delegated to another handler - verifyHeader - verify that a Request header matches an expected value
and also some utilities to wrap interceptors to change behaviour...
- whenStatus - apply an interceptor only for specific response statuses
- whenPattern - apply an interceptor only when the request URL matches a given pattern
These are in a similar vein to the by* functions of
@http/route, the naming convention of when* is
used to distinguish these interceptor functions from the routing functions.
Types of Interceptor
The intercept()
function supports registration of five different
types of interceptor:
These maybe supplied as the keys of an object, and you may supply a single
function or and array of functions for each type. You may also supply either a
single object or an array of these objects to the intercept function.
request, around and error interceptors are invoked in the order in which
they are supplied, whereas response and finally are applied in reverse
order. This ensures that a top-level feature that provides multiple types of
interceptor (such as a logger) would be the first to intercept the Request,
and the last to intercept the Response.
Request Interceptor
These are called before the Request is passed to the actual handler, and may
be used to modify or block requests entirely before they get to the handler.
The Request Interceptor is a function, that accepts the incoming Request, and
any additional arguments passed to the handler that is created by intercept.
It may return one of the following:
- Nothing (
undefined/void) - to indicate no action is to be taken, the originalRequestis free to pass to the next interceptor or to the handler (you may also just return the original Request) - A new or modified
Request- to replace the incoming request, to pass to the next interceptor or the handler - A
Response- to skip any further Request interceptors and the handler, this would pass to further Response interceptors though null(if allowed) - indicate that this handler as a whole cannot handle the request, this is used in combination withcascadeandwithFallbackfunctions of [@http/route] - but use ofnulland those functions is entirely optional.
The return value can optionally be wrapped in a Promise.
It may also throw an error, to be handled by the Error Interceptors.
So in our example above, we check the header and either return nothing, or an
unauthorized Response.
Response Interceptor
These are called with the outgoing Response, after the handler, or an
interceptor that returned a Response.
This is can be used, for example, to test whether something like a Not Modified response should be sent instead, or to render a full page for an error or a Not Found, or to add CORS or other custom headers.
Again, this just another function, that accepts the Request received by the
handler (or interceptor), and the Response returned from it.
It may return one of the following:
- Nothing (
undefined/void) - to indicate no action is to be taken, and the originalResponseshould be passed on - A new or modified
Response- to replace the outgoing request, to pass to the next interceptor or eventually out from the server null(if allowed) - indicate that this handler as a whole cannot handle the request, this is used in combination withcascadeandwithFallbackfunctions - but use ofnulland those functions is entirely optional.
The return value can optionally be wrapped in a Promise.
It may also throw an error, to be handled by the Error Interceptors.
Around Interceptor
This is very similar to the traditional middleware, these are called immediately surrounding the handler, and so after the request interceptors and before the response interceptors.
Unlike traditional middleware though, this isn't a catch-all replacement for those other interceptors above. It's really to allow side-effects and wrapping of the handler.
The interceptor function accepts the Request and a next function which it
MUST call and await.
A primary use-case for this is the
requestContext
interceptor, which stores the Request in an async context for later retrieval
(via getRequest) without having to explicitly passed it through every
function.
Use sparingly, request/response interceptors are generally more appropriate for most cases.
Error Interceptor
These are called if a Request or Response interceptor, or the handler throws an error. They can be used to provide a reasonable Response, and/or log the error.
An Error Interceptor function accepts the Request, optionally a Response (if
one has been produced so far), and the error (or other object) that was caught.
It may return one of the following:
- Nothing (
undefined/void) - if it is unable to handle the error - A
Response- to become the new outgoingResponse, this will be passed to further Error Interceptors, and may still be passed to Response Interceptors if the throw occurred from a Request Interceptor or the handler.
The return value can optionally be wrapped in a Promise.
If an Error Interceptor throws an error, it will NOT be handled by any other
interceptors and will immediately propagate out from the handler created by
intercept().
Finally Interceptor
These are called when a Response has completed or been aborted, and after all data from it's body has been drained.
This is useful for logging, recording metrics, and cleaning up resources after a request.
The interceptor function accepts the Request, optionally a Response (if one
was created before being aborted), and optionally the reason for the abort (from
the dispatched abort event).
It should not return anything, but anything that is returned is ignored. These
interceptors are not awaited.
It should not throw any errors, but any errors thrown are simply caught and logged to the console, and any further interceptors will continue to be called.
Shortcuts
interceptResponse()
Use of just Response Interceptors is the most common pattern, and so there is a
shortcut to intercept() for just those.
This example looks for static files first using the
staticRoute, which
results in a 404 response if the file is not found. So we use the
skip interceptor to convert
any 404 response to a null (unhandled) response, the request can then be
delegated to later handlers supplied to the
handle function...
Deno.serve( handle([ interceptResponse( staticRoute("/", import.meta.resolve("./public")), skip(404), ), byPattern("/hello", () => new Response("Hello")), ]), );