UCD.js Docs
FS Backend

HTTP Compatibility Guide

Build an HTTP application that is compatible with @ucdjs/fs-backend

This guide defines the standard for an HTTP application that should work with @ucdjs/fs-backend's built-in HTTP backend.

The goal is simple: if your service follows this contract, consumers can point HTTPFileSystemBackend at it and use the standard backend API without custom adapters.

Base Shape

The backend assumes a single baseUrl and issues requests relative to it:

  • GET {baseUrl}/{path} for files and directory listings
  • HEAD {baseUrl}/{path} for metadata and existence checks

Example:

import HTTPFileSystemBackend from "@ucdjs/fs-backend/backends/http";

const backend = HTTPFileSystemBackend({
  baseUrl: new URL("https://api.example.com/files"),
});

Path Rules

Your HTTP application should expose canonical backend paths:

  • all paths start with /
  • directories end with /
  • files do not end with /

Examples:

  • /16.0.0/ucd/
  • /16.0.0/ucd/UnicodeData.txt

GET Contract

File Reads

When a file path is requested with GET, return the raw file content.

  • 200 OK with the file body for existing files
  • 404 Not Found for missing files
  • other non-2xx responses are treated as backend errors

read() expects text and readBytes() expects binary-safe responses, so your application should return the real file bytes and an appropriate content type.

Directory Listings

When a directory path is requested with GET, return JSON.

Expected response shape:

[
  { "type": "file", "name": "UnicodeData.txt", "path": "/16.0.0/ucd/UnicodeData.txt" },
  { "type": "directory", "name": "emoji", "path": "/16.0.0/ucd/emoji/" }
]

Rules:

  • listings should be non-recursive
  • directory entries may omit children
  • extra fields like lastModified are tolerated
  • 404 means the directory does not exist
  • 403 should remain an error, not an empty directory

The HTTP backend handles recursive walking by following directory entries itself.

HEAD Contract

HEAD is the authoritative metadata endpoint for HTTP-compatible backends.

Required Header

  • X-UCD-Stat-Type: file or directory
  • X-UCD-Stat-Size: byte size for files
  • Last-Modified: modification timestamp, when available
  • Content-Type: content type for files

Example:

HTTP/1.1 200 OK
X-UCD-Stat-Type: file
X-UCD-Stat-Size: 12345
Last-Modified: Tue, 17 Mar 2026 10:00:00 GMT
Content-Type: text/plain; charset=utf-8

For directories:

HTTP/1.1 200 OK
X-UCD-Stat-Type: directory

Semantics Consumers Will See

exists()

exists() uses HEAD and is intentionally lossy:

  • 200 becomes true
  • 404 becomes false
  • callers should use stat() when they need more than a boolean

stat()

stat() uses HEAD and expects your server to provide X-UCD-Stat-Type.

  • 404 becomes BackendFileNotFound
  • type comes from X-UCD-Stat-Type
  • size prefers X-UCD-Stat-Size

Minimal Compatibility Checklist

Your HTTP application is compatible if it does all of the following:

  1. Serves files from GET.
  2. Serves non-recursive directory JSON from GET.
  3. Uses canonical backend paths in directory responses.
  4. Returns X-UCD-Stat-Type from HEAD.
  5. Returns 404 for missing paths.
  6. Does not turn authorization failures into fake empty directory listings.

Example Server Shape

The nicest way to structure this is with a small Request/Response helper. That keeps the actual compatibility logic framework-agnostic, and both Hono and H3 can call it directly.

function isNotFoundError(error: unknown): boolean {
  return error instanceof Error && error.message === "Not Found";
}

async function handleFilesRequest(method: "GET" | "HEAD", path: string): Promise<Response> {
  let stat;
  try {
    stat = await getStat(path);
  } catch (error) {
    if (isNotFoundError(error)) {
      return new Response(null, { status: 404 });
    }

    throw error;
  }

  if (method === "HEAD") {
    const headers = new Headers({
      "X-UCD-Stat-Type": stat.type,
    });

    if (stat.type === "file") {
      headers.set("X-UCD-Stat-Size", String(stat.size));
    }
    if (stat.mtime) {
      headers.set("Last-Modified", stat.mtime.toUTCString());
    }

    return new Response(null, {
      status: 200,
      headers,
    });
  }

  if (isDirectory(path)) {
    return Response.json(await listDirectory(path));
  }

  return new Response(await readFile(path));
}

Hono Adapter

import { Hono } from "hono";

const app = new Hono();

app.get("/files/*", (c) => {
  const path = c.req.path.replace(/^\/files/, "") || "/";
  return handleFilesRequest("GET", path);
});

app.head("/files/*", (c) => {
  const path = c.req.path.replace(/^\/files/, "") || "/";
  return handleFilesRequest("HEAD", path);
});

H3 Adapter

import { getRequestURL, H3 } from "h3";

const app = new H3();

app.get("/files/**", (event) => {
  const path = getRequestURL(event).pathname.replace(/^\/files/, "") || "/";
  return handleFilesRequest("GET", path);
});

app.head("/files/**", (event) => {
  const path = getRequestURL(event).pathname.replace(/^\/files/, "") || "/";
  return handleFilesRequest("HEAD", path);
});

On this page