Defjs

Attach Authorization and replay once inside one interceptor; next() only goes inward.

Refresh a Bearer token once on 401

next() is the rest of this onion — not axios(config) re-dispatch from the outside. Put attach + single-flight refresh + one replay in one HTTP interceptor so outer layers do not run twice.

See Interceptors.

import {
  createClient,
  createHttpInterceptor,
  defineRequest,
  struct,
  withEndpoint,
  withHTTPHandle,
  withInterceptors,
} from "@defjs/core";

const getInvoice = defineRequest({
  method: "GET",
  path: "/v1/invoice",
  output: struct.object({ total: struct.number() }),
  error: struct.object({ code: struct.literal("expired_token") }),
});

type Credential = { accessToken: string };

function refreshingBearer(
  read: () => Credential,
  refresh: (stale: Credential) => Promise<Credential>,
) {
  let flight: Promise<Credential> | undefined;

  return createHttpInterceptor(async (request, next) => {
    const authorize = (credential: Credential) => {
      const headers = new Headers(request.headers);
      headers.set("authorization", `Bearer ${credential.accessToken}`);
      return { ...request, headers };
    };

    const used = read();
    const response = await next(authorize(used));
    const body = response.body;
    if (
      response.status !== 401 ||
      typeof body !== "object" ||
      body === null ||
      !("code" in body) ||
      body.code !== "expired_token"
    ) {
      return response;
    }

    const current = read();
    if (current.accessToken !== used.accessToken)
      return next(authorize(current));

    flight ??= refresh(used).finally(() => {
      flight = undefined;
    });
    const refreshed = await flight;
    return next(authorize(refreshed));
  });
}

let credential: Credential = { accessToken: "v1" };
const client = createClient(
  withEndpoint("https://billing.example.test"),
  withHTTPHandle(async (input, init) => {
    const auth = new Request(input, init).headers.get("authorization");
    if (auth === "Bearer v1")
      return Response.json({ code: "expired_token" }, { status: 401 });
    return Response.json({ total: 42 }, { status: 200 });
  }),
  withInterceptors(
    refreshingBearer(
      () => credential,
      async () => {
        credential = { accessToken: "v2" };
        return credential;
      },
    ),
  ),
);

const outcome = await getInvoice(client);
const [error, invoice] = outcome;

function reportInvoice(result: typeof outcome) {
  const [fault, body] = result;
  if (fault) {
    console.error(fault.code);
    return;
  }
  console.log(body.total);
}

reportInvoice(outcome);
42

Calling next again only re-enters interceptors inside this layer (and the Fetch handler). It does not re-run outer timing/auth wrappers — that is why 401 layout belongs in one interceptor, not a second call of the operation.

Edit this page on GitHub
Released under the MIT License. · Copyright © 2026 MunMunMiao

On this page