Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ The endpoint MUST accept:
- Request `Content-Type`: `application/json`
- Response `Content-Type`: `application/json`

A request whose `Content-Type` media type is not `application/json` MUST be rejected with the response defined in section 7 with `error.code` of `unsupported_media_type` and HTTP status `415`. Only the media type is significant: servers MUST ignore parameters such as `charset`, so `application/json; charset=utf-8` is accepted.

The endpoint MAY accept other methods (e.g. `OPTIONS` for CORS preflight) but their semantics are out of scope.

## 3. Authentication
Expand Down Expand Up @@ -158,16 +160,19 @@ HTTP status: `4xx` or `5xx`.

Registered error codes in v0.1:

| `code` | HTTP | Meaning |
|---------------------|------|-----------------------------------------------------------------|
| `bad_request` | 400 | Request body shape is invalid. |
| `sql_error` | 400 | The SQL is invalid or failed at runtime. |
| `not_allowed` | 400 | Statement shape is rejected by server policy. |
| `auth_error` | 401 | Missing or invalid credentials. |
| `permission_error` | 403 | Authenticated but not permitted to run the statement. |
| `payload_too_large` | 413 | Request body or result set exceeds a server limit. |
| `rate_limited` | 429 | Too many requests. |
| `internal_error` | 500 | Server malfunction. |
| `code` | HTTP | Meaning |
|--------------------------|------|-----------------------------------------------------------------|
| `bad_request` | 400 | Request body shape is invalid. |
| `sql_error` | 400 | The SQL is invalid or failed at runtime. |
| `not_allowed` | 400 | Statement shape is rejected by server policy. |
| `auth_error` | 401 | Missing or invalid credentials. |
| `permission_error` | 403 | Authenticated but not permitted to run the statement. |
| `payload_too_large` | 413 | Request body or result set exceeds a server limit. |
| `unsupported_media_type` | 415 | Request `Content-Type` media type is not `application/json`. |
| `rate_limited` | 429 | Too many requests. |
| `internal_error` | 500 | Server malfunction. |

`unsupported_media_type` is introduced in v0.2. Per section 11, a new registered error code is an additive change that increments the minor version; this spec has no patch level, so the code is not available to a server that advertises `0.1`.

Vendor codes carry the prefix `vendor:` (e.g. `vendor:cf_d1_quota_exceeded`). Clients SHOULD treat unknown `error.code` values as if they were the closest registered code by HTTP status family.

Expand Down Expand Up @@ -199,7 +204,7 @@ to indicate the maximum spec version they understand. Servers MAY use this for f

A v0.1 conforming server MUST:

1. Accept POST requests with `Content-Type: application/json` at one or more endpoint URLs.
1. Accept POST requests with `Content-Type: application/json` at one or more endpoint URLs, and reject other media types per section 2.
2. Accept both single-statement (section 4.1) and batch (section 4.2) request shapes.
3. Return the success envelopes defined in section 6 for successful execution.
4. Return the error envelope defined in section 7 for any failure, using the HTTP status codes in the table.
Expand Down
5 changes: 4 additions & 1 deletion conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A conforming http-sql v0.1 server passes the test cases below when probed at its endpoint URL with a valid bearer token.

Cases marked `(v0.2+)` exercise behavior introduced after v0.1 and are not required of a server that advertises `X-Http-Sql-Version: 0.1`.

This directory will contain a runnable TypeScript test suite. The current document defines the test cases that runner must implement, so server implementers can self-check before installing the runner.

## How conformance is claimed
Expand Down Expand Up @@ -29,7 +31,8 @@ Conformance is self-asserted. The community can call out failures via issues.
| R-1 | Body contains both `sql` and `batch` | 400, `error.code` = `bad_request` |
| R-2 | Body contains neither `sql` nor `batch` | 400, `error.code` = `bad_request` |
| R-3 | Body is not valid JSON | 400, `error.code` = `bad_request` |
| R-4 | `Content-Type` other than `application/json` | 400 or 415 |
| R-4 | `Content-Type` other than `application/json` (v0.2+) | 415, `error.code` = `unsupported_media_type` |
| R-5 | `Content-Type: application/json; charset=utf-8` | Executes normally -- media-type parameters are ignored |

### Single-statement execution

Expand Down
10 changes: 10 additions & 0 deletions examples/cloudflare-durable-object/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ app.post("/sql", async (c) => {
const tenant = resolveTenant(c.req.header("authorization") ?? "", c.env);
if (!tenant) return c.json({ error: { code: "auth_error", message: "missing or invalid bearer token" } }, 401);

if (!isJsonMediaType(c.req.header("content-type"))) {
return c.json({ error: { code: "unsupported_media_type", message: "Content-Type must be application/json" } }, 415);
}

const id = c.env.TENANT_DO.idFromName(tenant);
const stub = c.env.TENANT_DO.get(id);
return stub.fetch(c.req.raw);
Expand All @@ -43,6 +47,12 @@ app.onError((err, c) => {

export default app;

// SPEC.md section 2: only the media type is significant, so parameters such as
// `charset=utf-8` are ignored.
function isJsonMediaType(header: string | undefined): boolean {
return header?.split(";")[0].trim().toLowerCase() === "application/json";
}

function resolveTenant(header: string, env: Env): string | null {
const token = header.replace(/^Bearer\s+/i, "").trim();
if (!token) return null;
Expand Down
10 changes: 10 additions & 0 deletions examples/cloudflare-worker-to-d1/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ app.post(
"/",
async (c, next) => bearerAuth({ token: c.env.HTTP_SQL_TOKEN })(c, next),
async (c) => {
if (!isJsonMediaType(c.req.header("content-type"))) {
return c.json({ error: { code: "unsupported_media_type", message: "Content-Type must be application/json" } }, 415);
}

let body: SingleRequest | BatchRequest;
try { body = await c.req.json(); }
catch { return c.json({ error: { code: "bad_request", message: "invalid JSON" } }, 400); }
Expand Down Expand Up @@ -115,6 +119,12 @@ function projectD1Result(res: D1Result): StatementResult {
};
}

// SPEC.md section 2: only the media type is significant, so parameters such as
// `charset=utf-8` are ignored.
function isJsonMediaType(header: string | undefined): boolean {
return header?.split(";")[0].trim().toLowerCase() === "application/json";
}

// Tagged values per SPEC.md section 5.
function decodeParam(value: unknown): unknown {
if (value && typeof value === "object" && "$type" in value && "$value" in value) {
Expand Down
9 changes: 9 additions & 0 deletions examples/reference-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const JSON_HEADERS = { "content-type": "application/json", ...VERSION_HEADER };
export async function handle(req: Request, auth: (req: Request) => boolean): Promise<Response> {
if (!auth(req)) return errorResponse(401, "auth_error", "missing or invalid bearer token");
if (req.method !== "POST") return errorResponse(405, "bad_request", "POST required");
if (!isJsonMediaType(req.headers.get("content-type"))) {
return errorResponse(415, "unsupported_media_type", "Content-Type must be application/json");
}

let body: RequestBody;
try { body = await req.json(); }
Expand All @@ -47,6 +50,12 @@ export async function handle(req: Request, auth: (req: Request) => boolean): Pro
}
}

// SPEC.md section 2: only the media type is significant, so parameters such as
// `charset=utf-8` are ignored.
function isJsonMediaType(header: string | null): boolean {
return header?.split(";")[0].trim().toLowerCase() === "application/json";
}

// Replace these with calls to your actual database client.
async function execute(_stmt: Statement): Promise<Result> {
return { columns: [], rows: [], rowsAffected: 0, lastInsertId: null };
Expand Down
Loading