Accessing HTTP request and response objects
To build REST APIs with LoopBack, it’s highly recommended for applications to not directly manipulate HTTP (Express) request/response objects. Instead, we should use OpenAPI decorators to:
-
Describe operation parameters and body so that such values will be injected as part of request processing.
-
Return a value or promise for the response so that the framework can serialize it into HTTP responses.
For example:
import {get} from '@loopback/rest';
import {inject} from '@loopback/core';
export class PingController {
constructor() {}
// Map to `GET /ping`
@get('/ping', {
responses: {
'200': {
description: 'Ping Response',
content: {
'application/json': {
schema: {
type: 'object',
title: 'PingResponse',
properties: {
greeting: {type: 'string'},
date: {type: 'string'},
},
},
},
},
},
},
})
ping(@param.query.string('message') msg: string): object {
return {
greeting: `[Pong] ${msg}`,
date: new Date(),
};
}
}
There are obvious benefits to stick with the best practice:
- Allow validation of input/output based on OpenAPI specs.
- Allow strongly-typed access to input/output data.
But for some use cases, it’s still desirable to access HTTP request and response objects, as most Express middleware and routes do. Here are some examples:
- Access request headers that are not described by the OpenAPI spec
- Access the request stream
- Set status code of responses
- Set extra response headers that are not described by the OpenAPI spec
Note:
Eventually, we would like to implement first-class support for controllers to set response status code and headers without having to access the Express response object. The feature request is tracked by github issue #436
In @loopback/rest
, we wrap Express HTTP request/response into a
RequestContext
object and bind it to
RestBindings.Http.CONTEXT
key. This makes it possible to access Express request/response objects via
dependency injection for controllers, services, and other artifacts.
Inject HTTP request
The Express/Http request object can
be injected via RestBindings.Http.REQUEST
.
import {Request, RestBindings} from '@loopback/rest';
import {inject} from '@loopback/core';
export class PingController {
constructor(@inject(RestBindings.Http.REQUEST) private request: Request) {}
ping(): object {
// Access the request object via `this.request`
const = url: this.request.url,
const headers = {...this.request.headers};
// ...
}
}
In order to receive injection of RestBinding.Http.REQUEST
,
RestBinding.Http.RESPONSE
, or RestBinding.Http.CONTEXT
, the controller class
needs to have binding scope set to BindingScope.TRANSIENT
(which is the
default if not set). Alteratively, a controller can receive HTTP
request/response/context objects via method parameter injection.
import {Request, RestBindings} from '@loopback/rest';
import {injectable, inject, BindingScope} from '@loopback/core';
@injectable({scope: BindingScope.SINGLETON})
export class PingController {
ping(
// Inject via
@inject(RestBindings.Http.REQUEST) request: Request,
): object {
// Access the request object via `request`
const = url: request.url,
const headers = {...request.headers};
// ...
}
}
Inject HTTP response
The Express/Http response object can
be injected via RestBindings.Http.RESPONSE
.
import {Response, RestBindings} from '@loopback/rest';
import {inject} from '@loopback/core';
export class PingController {
constructor(@inject(RestBindings.Http.RESPONSE) private response: Response) {}
@get('/ping')
ping(): Response {
// Access the response object via `this.response`
this.response.status(200).send({
greeting: 'Hello from LoopBack',
date: new Date(),
});
// Return the HTTP response object so that LoopBack framework skips the
// generation of HTTP response
return this.response;
}
@get('/header')
header(): string {
// Set custom http response header
this.response.set('x-custom-res-header', 'xyz');
return 'Hello';
}
}
Inject HTTP request context
The Express/Http request/response object can be injected via
RestBindings.Http.CONTEXT
.
import {RequestContext, RestBindings} from '@loopback/rest';
import {inject} from '@loopback/core';
export class PingController {
constructor(
@inject(RestBindings.Http.CONTEXT) private requestCtx: RequestContext,
) {}
ping(): Response {
const {request, response} = this.requestCtx;
this.response.status(200).send({
greeting: 'Hello from LoopBack',
date: new Date(),
url: this.req.url,
headers: Object.assign({}, this.req.headers),
});
// Return the HTTP response object so that LoopBack framework skips the
// generation of HTTP response
return response;
}
}
Use middleware to access HTTP request/response
REST middleware-based sequence allows middleware to access HTTP request context as follows:
import {Middleware, MiddlewareContext} from '@loopback/rest';
import {debugFactory} from 'debug';
import {v1} from 'uuid';
const trace = debugFactory('trace:request-response');
/**
* An middleware for tracing HTTP requests/responses
* @param ctx - Context object
* @param next - Downstream middleware/handlers
*/
export const tracingMiddleware: Middleware = async (ctx, next) => {
setupRequestId(ctx);
const {request, response} = ctx;
try {
if (trace.enabled) {
const reqObj = {
method: request.method,
originalUrl: request.originalUrl,
headers: request.headers,
// Body is not available yet before `parseParams`
};
trace('Request: %s', reqObj);
}
const result = await next();
if (trace.enabled) {
const resObj = {
statusCode: response.statusCode,
headers: response.getHeaders(),
};
trace('Response: %s', resObj);
}
return result;
} catch (err) {
if (trace.enabled) {
trace('Error: %s', err);
}
throw err;
}
};
function setupRequestId(ctx: MiddlewareContext) {
let requestId = ctx.request?.get('X-Request-ID');
debug(
'RequestID for %s %s: %s',
ctx.request.method,
ctx.request.originalUrl,
requestId,
);
if (requestId == null) {
requestId = v1();
debug(
'A new RequestID is generated for %s %s: %s',
ctx.request.method,
ctx.request.originalUrl,
requestId,
);
}
// Bind `request.id` so that it is available for injection in downstream
// controllers/services
ctx.bind('request.id').to(requestId);
}
Using interceptors to access HTTP request/response
Interceptors can access HTTP request context as follows:
import {Interceptor, InvocationContext} from '@loopback/core';
import {RestBinding} from '@loopback/rest';
/**
* An interceptor to access HTTP requests/responses
* @param ctx - Invocation Context object
* @param next - Downstream interceptors
*/
export const myInterceptor: Interceptor = async (ctx, next) => {
const reqCtx = await ctx.get(RestBinding.Http.CONTEXT);
// ...
};
Interceptors can be wrapped into a provider class to allow dependency injections so that http request/response/context objects can be injected as follows:
export class MyInterceptorProvider implements Provider<Middleware> {
constructor(
@inject(RestBindings.Http.CONTEXT) private requestCtx: RequestContext,
) {}
value(): Interceptor {
return async (ctx, next) => {
// Access RequestContext via `this.requestCtx`
// ...
};
}
}