@lodestar/api
Version:
A Typescript REST client for the Ethereum Consensus API
150 lines (139 loc) • 5.88 kB
text/typescript
import type * as fastify from "fastify";
import {HttpHeader, MediaType, SUPPORTED_MEDIA_TYPES, parseAcceptHeader, parseContentTypeHeader} from "../headers.js";
import {
Endpoint,
JsonRequestData,
JsonRequestMethods,
RequestData,
RequestWithBodyCodec,
RequestWithoutBodyCodec,
RouteDefinition,
SszRequestData,
SszRequestMethods,
isRequestWithoutBody,
} from "../types.js";
import {WireFormat, fromWireFormat, getWireFormat} from "../wireFormat.js";
import {ApiError} from "./error.js";
import {ApplicationMethod} from "./method.js";
export type FastifyHandler<E extends Endpoint> = fastify.RouteHandlerMethod<
fastify.RawServerDefault,
fastify.RawRequestDefaultExpression<fastify.RawServerDefault>,
fastify.RawReplyDefaultExpression<fastify.RawServerDefault>,
{
Body: E["request"] extends JsonRequestData ? E["request"]["body"] : undefined;
Querystring: E["request"]["query"];
Params: E["request"]["params"];
Headers: E["request"]["headers"];
},
fastify.ContextConfigDefault
>;
export function createFastifyHandler<E extends Endpoint>(
definition: RouteDefinition<E>,
method: ApplicationMethod<E>,
_operationId: string
): FastifyHandler<E> {
return async (req, resp) => {
// Determine response wire format first to inform application method
// about the preferable return type to avoid unnecessary serialization
let responseMediaType: MediaType | null;
const acceptHeader = req.headers.accept;
if (definition.resp.isEmpty) {
// Ignore Accept header, the response will be sent without body
responseMediaType = null;
} else if (acceptHeader === undefined) {
// Default to json to not force user to set header, e.g. when using curl
responseMediaType = MediaType.json;
} else {
const {onlySupport} = definition.resp;
const supportedMediaTypes = onlySupport !== undefined ? [fromWireFormat(onlySupport)] : SUPPORTED_MEDIA_TYPES;
responseMediaType = parseAcceptHeader(acceptHeader, supportedMediaTypes);
if (responseMediaType === null) {
throw new ApiError(406, `Accepted media types not supported: ${acceptHeader}`);
}
}
const responseWireFormat = responseMediaType !== null ? getWireFormat(responseMediaType) : null;
let requestWireFormat: WireFormat | null;
if (isRequestWithoutBody(definition)) {
requestWireFormat = null;
} else {
const contentType = req.headers[HttpHeader.ContentType];
if (contentType === undefined && req.body === undefined) {
// Default to json parser if body is omitted. This is not possible for most
// routes as request will fail schema validation before this handler is called
requestWireFormat = WireFormat.json;
} else {
if (contentType === undefined) {
throw new ApiError(400, "Content-Type header is required");
}
const requestMediaType = parseContentTypeHeader(contentType);
if (requestMediaType === null) {
throw new ApiError(415, `Unsupported media type: ${contentType.split(";", 1)[0]}`);
}
requestWireFormat = getWireFormat(requestMediaType);
}
const {onlySupport} = definition.req as RequestWithBodyCodec<E>;
if (onlySupport !== undefined && onlySupport !== requestWireFormat) {
throw new ApiError(415, `Endpoint only supports ${onlySupport.toUpperCase()} requests`);
}
}
let args: E["args"];
try {
switch (requestWireFormat) {
case WireFormat.json:
args = (definition.req as JsonRequestMethods<E>).parseReqJson(req as JsonRequestData);
break;
case WireFormat.ssz:
args = (definition.req as SszRequestMethods<E>).parseReqSsz(req as SszRequestData<E["request"]>);
break;
case null:
args = (definition.req as RequestWithoutBodyCodec<E>).parseReq(req as RequestData);
break;
}
} catch (e) {
if (e instanceof ApiError) throw e;
// Errors related to parsing should return 400 status code
throw new ApiError(400, (e as Error).message);
}
const response = await method(args, {
sszBytes: requestWireFormat === WireFormat.ssz ? (req.body as Uint8Array) : null,
returnBytes: responseWireFormat === WireFormat.ssz,
});
if (response?.status !== undefined) {
resp.statusCode = response.status;
}
switch (responseWireFormat) {
case WireFormat.json: {
const metaHeaders = definition.resp.meta.toHeadersObject(response?.meta);
metaHeaders[HttpHeader.ContentType] = MediaType.json;
void resp.headers(metaHeaders);
const data =
response?.data instanceof Uint8Array
? definition.resp.data.toJson(definition.resp.data.deserialize(response.data, response.meta), response.meta)
: definition.resp.data.toJson(response?.data, response?.meta);
const metaJson = definition.resp.meta.toJson(response?.meta);
if (definition.resp.transform) {
return definition.resp.transform.toResponse(data, metaJson);
}
return {
data,
...(metaJson as object),
};
}
case WireFormat.ssz: {
const metaHeaders = definition.resp.meta.toHeadersObject(response?.meta);
metaHeaders[HttpHeader.ContentType] = MediaType.ssz;
void resp.headers(metaHeaders);
const data =
response?.data instanceof Uint8Array
? response.data
: definition.resp.data.serialize(response?.data, response?.meta);
// Fastify supports returning `Uint8Array` from handler and will efficiently
// convert it to a `Buffer` internally without copying the underlying `ArrayBuffer`
return data;
}
case null:
// Send response without body
return;
}
};
}