Skip to content
File

Blob: src/client/lib/api/http.ts

typescript86 lines
1import { ErrorResponse } from "@/shared/contracts";
2import { D1_BOOKMARK_HEADER } from "@/shared/d1";
3import { applyResponseBookmark, getStoredBookmark } from "./bookmarks";
4import { type ApiRequestOptions, type Decoder } from "./shared";
5 
6export class ApiError extends Error {
7 constructor(
8 message: string,
9 readonly status: number,
10 readonly code?: string,
11 ) {
12 super(message);
13 this.name = "ApiError";
14 }
15}
16 
17export async function parseError(response: Response) {
18 const fallback = `${response.status} ${response.statusText}`.trim();
19 
20 try {
21 const payload = ErrorResponse.assertDecode(await response.json());
22 return {
23 code: payload.code,
24 message: payload.error ?? fallback,
25 };
26 } catch {
27 return {
28 message: fallback,
29 };
30 }
31}
32 
33export function encodeJsonBody<T>(decoder: Decoder<T>, value: unknown) {
34 return JSON.stringify(decoder.assertDecode(value));
35}
36 
37export async function fetchWithSession(path: string, options?: ApiRequestOptions) {
38 const { token, bookmarkScope, onBookmark, ...requestInit } = options ?? {};
39 const headers = new Headers(requestInit.headers);
40 
41 if (token) {
42 headers.set("authorization", `Bearer ${token}`);
43 }
44 
45 if (bookmarkScope) {
46 const bookmark = getStoredBookmark(bookmarkScope);
47 if (bookmark) {
48 headers.set(D1_BOOKMARK_HEADER, bookmark);
49 }
50 }
51 
52 const response = await fetch(path, {
53 ...requestInit,
54 headers,
55 });
56 
57 applyResponseBookmark(response, { bookmarkScope, onBookmark });
58 
59 return response;
60}
61 
62export async function throwApiError(response: Response): Promise<never> {
63 const { code, message } = await parseError(response);
64 throw new ApiError(message, response.status, code);
65}
66 
67export async function request<T>(path: string, decoder: Decoder<T>, options?: ApiRequestOptions) {
68 const headers = new Headers(options?.headers);
69 headers.set("accept", "application/json");
70 
71 if (options?.body && !headers.has("content-type")) {
72 headers.set("content-type", "application/json");
73 }
74 
75 const response = await fetchWithSession(path, {
76 ...options,
77 headers,
78 });
79 
80 if (!response.ok) {
81 await throwApiError(response);
82 }
83 
84 return decoder.assertDecode(await response.json());
85}