@lodestar/api
Version:
A Typescript REST client for the Ethereum Consensus API
210 lines (181 loc) • 5.91 kB
text/typescript
import {HeadersExtra, HttpHeader, parseContentTypeHeader} from "../headers.js";
import {HttpStatusCode} from "../httpStatusCode.js";
import {Endpoint} from "../types.js";
import {WireFormat, getWireFormat} from "../wireFormat.js";
import {ApiError} from "./error.js";
import {RouteDefinitionExtra} from "./request.js";
// TODO: Workaround for tsgo import-elision bug: ensure this is treated as a runtime value.
// https://github.com/microsoft/typescript-go/issues/2212
void HttpStatusCode;
export type RawBody =
| {type: WireFormat.json; value: unknown}
| {type: WireFormat.ssz; value: Uint8Array}
| {type?: never; value?: never};
export class ApiResponse<E extends Endpoint> extends Response {
private definition: RouteDefinitionExtra<E>;
private _wireFormat?: WireFormat | null;
private _rawBody?: RawBody;
private _errorBody?: string;
private _meta?: E["meta"];
private _value?: E["return"];
constructor(definition: RouteDefinitionExtra<E>, body?: BodyInit | null, init?: ResponseInit) {
super(body, init);
this.definition = definition;
}
wireFormat(): WireFormat | null {
if (this._wireFormat === undefined) {
if (this.definition.resp.isEmpty) {
this._wireFormat = null;
return this._wireFormat;
}
const contentType = this.headers.get(HttpHeader.ContentType);
if (contentType === null) {
if (this.status === HttpStatusCode.NO_CONTENT) {
this._wireFormat = null;
return this._wireFormat;
}
throw Error("Content-Type header is required in response");
}
const mediaType = parseContentTypeHeader(contentType);
if (mediaType === null) {
throw Error(`Unsupported response media type: ${contentType.split(";", 1)[0]}`);
}
const wireFormat = getWireFormat(mediaType);
const {onlySupport} = this.definition.resp;
if (onlySupport !== undefined && wireFormat !== onlySupport) {
throw Error(`Method only supports ${onlySupport.toUpperCase()} responses`);
}
this._wireFormat = wireFormat;
}
return this._wireFormat;
}
async rawBody(): Promise<RawBody> {
this.assertOk();
if (!this._rawBody) {
switch (this.wireFormat()) {
case WireFormat.json:
this._rawBody = {
type: WireFormat.json,
value: await super.json(),
};
break;
case WireFormat.ssz:
this._rawBody = {
type: WireFormat.ssz,
value: new Uint8Array(await this.arrayBuffer()),
};
break;
default:
this._rawBody = {};
}
}
return this._rawBody;
}
meta(): E["meta"] {
this.assertOk();
if (!this._meta) {
switch (this.wireFormat()) {
case WireFormat.json: {
const rawBody = this.resolvedRawBody();
const metaJson = this.definition.resp.transform
? this.definition.resp.transform.fromResponse(rawBody.value).meta
: rawBody.value;
this._meta = this.definition.resp.meta.fromJson(metaJson);
break;
}
case WireFormat.ssz:
this._meta = this.definition.resp.meta.fromHeaders(new HeadersExtra(this.headers));
break;
}
}
return this._meta;
}
value(): E["return"] {
this.assertOk();
if (!this._value) {
const rawBody = this.resolvedRawBody();
const meta = this.meta();
switch (rawBody.type) {
case WireFormat.json: {
const dataJson = this.definition.resp.transform
? this.definition.resp.transform.fromResponse(rawBody.value).data
: (rawBody.value as Record<string, unknown>)?.data;
this._value = this.definition.resp.data.fromJson(dataJson, meta);
break;
}
case WireFormat.ssz:
this._value = this.definition.resp.data.deserialize(rawBody.value, meta);
break;
}
}
return this._value;
}
ssz(): Uint8Array {
this.assertOk();
const rawBody = this.resolvedRawBody();
switch (rawBody.type) {
case WireFormat.json:
return this.definition.resp.data.serialize(this.value(), this.meta());
case WireFormat.ssz:
return rawBody.value;
default:
return new Uint8Array();
}
}
json(): Awaited<ReturnType<Response["json"]>> {
this.assertOk();
const rawBody = this.resolvedRawBody();
switch (rawBody.type) {
case WireFormat.json:
return rawBody.value;
case WireFormat.ssz:
return this.definition.resp.data.toJson(this.value(), this.meta());
default:
return {};
}
}
assertOk(): void {
if (!this.ok) {
throw this.error();
}
}
error(): ApiError | null {
if (this.ok) {
return null;
}
return new ApiError(this.getErrorMessage(), this.status, this.definition.operationId);
}
async errorBody(): Promise<string> {
if (this._errorBody === undefined) {
this._errorBody = await this.text();
}
return this._errorBody;
}
private resolvedRawBody(): RawBody {
if (!this._rawBody) {
throw Error("rawBody() must be called first");
}
return this._rawBody;
}
private resolvedErrorBody(): string {
if (this._errorBody === undefined) {
throw Error("errorBody() must be called first");
}
return this._errorBody;
}
private getErrorMessage(): string {
const errBody = this.resolvedErrorBody();
try {
const errJson = JSON.parse(errBody) as {message?: string; failures?: {message: string}[]};
if (errJson.message) {
if (errJson.failures) {
return `${errJson.message}\n` + errJson.failures.map((e) => e.message).join("\n");
}
return errJson.message;
}
return errBody;
} catch (_e) {
return errBody || this.statusText;
}
}
}