Context
Introduction
The context object is passed as the first parameter to a middleware function. It includes the HTTP request, the HTTP response, and any additional properties added by previous middleware calls.
Usage
import { Kernel } from "@raptor/kernel";
import type { Context } from "@raptor/types";
const app = new Kernel();
app.use((context: Context) => {
const { request, response } = context;
console.log(request, response);
return "Dinosaurs eat man. Woman inherits the earth";
});
app.serve();Request
Raptor uses the standard Web API Request object, giving you access to all its built-in properties and methods without any framework-specific wrappers.
app.use((context: Context) => {
const { request } = context;
console.log(request.method);
console.log(request.url);
console.log(request.headers);
return "Life, uh, finds a way";
});Common Request Operations
Reading Headers
app.use((context: Context) => {
const authToken = context.request.headers.get("Authorization");
const contentType = context.request.headers.get("Content-Type");
return { authToken, contentType };
});Parsing Request Body
app.use(async (context: Context) => {
// JSON body
const data = await context.request.json();
// Form data
const formData = await context.request.formData();
// Plain text
const text = await context.request.text();
return { received: data };
});Reading Query Parameters
app.use((context: Context) => {
const url = new URL(context.request.url);
const name = url.searchParams.get("name");
const page = url.searchParams.get("page") || "1";
return { name, page };
});Checking Request Method
import { MethodNotAllowed } from "@raptor/kernel";
app.use((context: Context) => {
if (context.request.method === "POST") {
// Handle POST request
}
if (context.request.method !== "GET") {
throw new MethodNotAllowed();
}
return "Clever Girl";
});Since Raptor doesn't wrap or modify the Request object, you can use any Web API features directly - no need to learn framework-specific methods.
Connection
The Web API Request object says nothing about the network the request arrived on, so Raptor exposes that separately as context.connection. Each server adapter reads the peer from its own runtime - Deno, Bun and Node all report a socket - and normalises it into the same shape.
app.use((context: Context) => {
const { connection } = context;
return {
address: connection?.remote.address,
port: connection?.remote.port,
type: connection?.remote.addressType,
};
});The connection object
| Property | Type | Description |
|---|---|---|
remote.address | string | The remote IP address, in canonical form |
remote.port | number | The remote port |
remote.addressType | "IPv4" | "IPv6" | The address family |
Addresses are canonicalised before they reach you, so the same host always arrives as the same string - ::ffff:127.0.0.1 is reported as 127.0.0.1, and 2001:0db8::0001 as 2001:db8::1. That makes the value safe to compare, log, or use as a cache or rate-limit key without normalising it yourself. An address the parser doesn't recognise is dropped rather than passed through.
When there is no connection
context.connection is optional, and so is every field on it, because not every request arrives over a socket the runtime can describe:
- A request over a Unix socket has no network address.
- A serverless deployment has no socket at all. The Lambda adapter fills in the caller's address from the API Gateway event, which means there is an address but never a port.
- Middleware tested by calling it directly, or a request dispatched in-process, has no connection.
Always treat it as absent-by-default and reach for it optionally:
app.use((context: Context) => {
const address = context.connection?.remote.address;
if (!address) {
return { message: "You didn't say the magic word" };
}
return { address };
});Behind a proxy
context.connection is deliberately the socket peer, and nothing else - it is the one address the runtime can vouch for. Run behind a load balancer, a CDN, or an ingress controller and that address will be the proxy, not the client.
To resolve the client, use the clientAddress helper. It starts from the socket peer and walks the X-Forwarded-For chain outwards while each hop is trusted, returning the first address it can't account for. Anchoring on the socket is what stops a forged header reaching past your trust boundary.
import { clientAddress } from "@raptor/kernel";
app.use((context: Context) => {
// Trusts nothing, so this is just the socket peer.
const socket = clientAddress(context);
// Trusts hops inside the given ranges, stopping at the first that isn't.
const client = clientAddress(context, ["10.0.0.0/8", "172.16.0.0/12"]);
return { socket, client };
});The second argument is the trust policy:
| Policy | Behaviour |
|---|---|
false (default) | Ignore X-Forwarded-For entirely and use the socket peer |
string[] | Trust hops falling inside these CIDR ranges, stopping at the first that isn't |
number | Trust that many proxy hops, counting out from the socket |
Prefer the CIDR list. A hop count checks no addresses, so it is only safe where your application cannot be reached except through exactly that many proxies - if a request can also arrive by a shorter path, such as an exposed origin or a second ingress, then whoever sends the header picks the address you get back. There is no "trust everything" setting, and a range matching every address is rejected, for the same reason.
Compiling the policy once
A policy passed to clientAddress is revalidated on every call. Where you use one address per request, compile it at start-up with compileTrust instead. An invalid range then throws while the application is booting, rather than on a request:
import { clientAddress, compileTrust } from "@raptor/kernel";
const trusted = compileTrust(["10.0.0.0/8"]);
app.use((context: Context) => {
const address = clientAddress(context, trusted);
return { address };
});Response
In Raptor, at least one middleware function is required to return a body response to ensure that the request cycle completes successfully. Raptor automatically handles response types through its response processor system. By default, it can process JSON objects, HTML strings, plain text, and raw Response objects. However, you can extend this system with your own custom processors.
Returning JSON
If the middleware response body is an object, it will be automatically recognized as application/json. Consequently, both the Content-Type header and the response body will be appropriately set to reflect this format.
app.use(() => ({
name: "Dr Ian Malcolm",
}));Returning HTML
When a string is returned, the Content-Type header is automatically set to text/plain. However, if the string is detected to contain HTML, the Content-Type header will be automatically adjusted to text/html.
app.use(() => "<h1>Hello, Dr Malcolm!</h1>");Returning Response Directly
Raptor makes it easy to return simple scalar values as responses, but when you need full control over the output, you can also return a custom Response object directly.
app.use(() => new Response("We spared no expense", { status: 200 }));Overriding headers
Although it's convenient to return data without configuring a Content-Type, there may be instances where you need to specify a particular header. In such cases, you can proceed as follows:
app.use((context: Context) => {
context.response.headers.set("Content-Type", "application/hal+json");
return {
name: "That doesn't look very scary. More like a six-foot turkey.",
};
});Extending
Response Processors
What are Response Processors?
The Response Manager handles converting your middleware return values into HTTP responses. There are four built-in processors out of the box, but you can add custom ones or override the defaults.
Built-in Processors
| Type | Handles | Content-Type |
|---|---|---|
response | Response objects | (preserved from original) |
error | Error instances | Based on request Accept header |
string | String values | text/plain or text/html |
object | Objects/Arrays | application/json |
Creating a custom processor
To create a custom processor, implement the Processor interface:
import type { Context } from "@raptor/types";
import type { Processor, ResponseBodyType } from "@raptor/kernel";
export default class CustomStringProcessor implements Processor {
process(body: any, context: Context): Response {
return new Response("custom string processor: " + body, {
status: context.response.status,
headers: context.response.headers,
});
}
}Overriding an existing processor
Configuration:
It's easy to override existing processors using configuration-based replacement:
import type { Context } from "@raptor/types";
import { Kernel, ResponseManager } from "@raptor/kernel";
import CustomStringProcessor from "./custom-string-processor.ts";
const app = new Kernel({
response: {
processors: {
string: new CustomStringProcessor(),
},
},
});
app.use(() => "Hello, Dr Malcom!");
app.serve();
// custom string processor: Hello, Dr Malcolm!Using the class methods:
Or if you prefer class-based methods, you can do the following:
import type { Context } from "@raptor/types";
import { Kernel, ResponseManager } from "@raptor/kernel";
import CustomStringProcessor from "./custom-string-processor.ts";
const app = new Kernel();
const manager = new ResponseManager();
manager.register("string", new CustomStringProcessor());
app.setResponseManager(manager);
app.use(() => "Hello, Dr Malcom!");
app.serve();
// custom string processor: Hello, Dr Malcolm!All strings which are returned from middleware will now use your custom processor.