< Summary

Information
Class: http.ts
Assembly: app.api
File(s): /home/runner/work/ClutterStock/ClutterStock/frontend/app/api/http.ts
Tag: 58_25416222083
Line coverage
96%
Covered lines: 28
Uncovered lines: 1
Coverable lines: 29
Total lines: 72
Line coverage: 96.5%
Branch coverage
83%
Covered branches: 15
Total branches: 18
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/ClutterStock/ClutterStock/frontend/app/api/http.ts

#LineLine coverage
 1import { getValidToken } from "~/lib/oidc.server";
 2import { apiPathVersionPrefix, getApiBase } from "~/constants/api";
 3
 4export type ApiLogEvent =
 5  | { kind: "request"; method: string; url: string }
 6  | { kind: "response"; method: string; url: string; status: number; durationMs: number }
 7  | { kind: "error"; method: string; url: string; error: unknown };
 8
 9let logListener: ((e: ApiLogEvent) => void) | undefined;
 10
 2311export function setApiLogListener(
 12  listener: ((e: ApiLogEvent) => void) | undefined,
 13): void {
 2314  logListener = listener;
 15}
 16
 2017function resolveUrl(path: string): string {
 2018  const base = getApiBase().replace(/\/$/, "");
 19  // Paths beginning with "/api/" are treated as already-prefixed (the typed
 20  // wrapper supplies them straight from the openapi `paths` keys). Other
 21  // paths are prefixed with the version segment for legacy callers.
 2022  const versionedPath = path.startsWith("/api/")
 23    ? path.replace(/^\//, "")
 24    : `${apiPathVersionPrefix}/${path.replace(/^\//, "")}`;
 2025  return base ? `${base}/${versionedPath}` : `/${versionedPath}`;
 26}
 27
 28/**
 29 * All API traffic goes through the BFF (React Router SSR server).
 30 * Pass `ssrRequest` from every loader/action so the session cookie can be
 31 * resolved to a Bearer token server-side.  Browser-direct .NET calls are gone.
 32 */
 2033export async function apiFetch(
 34  path: string,
 35  init: RequestInit = {},
 36  ssrRequest?: Request,
 37): Promise<Response> {
 2038  const url = resolveUrl(path);
 2039  const method = (init.method ?? "GET").toUpperCase();
 40
 2041  const headers = new Headers(init.headers);
 42
 2043  if (ssrRequest) {
 244    const token = await getValidToken(ssrRequest);
 245    if (token) headers.set("Authorization", `Bearer ${token}`);
 46  }
 47
 2048  logListener?.({ kind: "request", method, url });
 2049  if (import.meta.env.DEV) console.debug(`[api] ${method} ${url}`);
 50
 2051  const t0 = Date.now();
 52
 2053  try {
 2054    const response = await fetch(url, { ...init, headers });
 1955    const durationMs = Date.now() - t0;
 1956    logListener?.({ kind: "response", method, url, status: response.status, durationMs });
 2057    if (import.meta.env.DEV)
 1958      console.debug(`[api] ${method} ${url} → ${response.status} (${durationMs}ms)`);
 59
 1960    if (response.status === 401) {
 61      // Session expired and refresh failed — redirect to sign-in from server
 062      throw new Response(null, { status: 302, headers: { Location: "/auth/signin" } });
 63    }
 64
 1965    return response;
 66  } catch (error) {
 167    if (error instanceof Response) throw error; // propagate redirects
 168    logListener?.({ kind: "error", method, url, error });
 169    console.error(`[api] ${method} ${url} failed`, error);
 170    throw error;
 71  }
 72}