Hmbown/CodeWhale · info

POST

Error message

POST

What it means

The telemetry-ingest worker's ingest() deliberately returns 405 with an `allow: POST` header for any non-POST request, before checking the path. This is intentional: probing any path with any verb other than POST yields the same answer so scanners learn nothing about existing endpoints. It surfaces as the HTTP error "POST" reported at the ingest boundary.

Solutions

  1. Send requests as POST to the ingest path
  2. Point health checks at a dedicated health endpoint instead of the ingest path
  3. Update the client so it uses method: "POST" with a JSON body
  4. Expect a 405 with allow: POST header when probing with other verbs — this is by design

Example fix

// before
await fetch(url); // GET
// after
await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
Defensive patterns

Strategy: try-catch

Validate before calling

if (method.toUpperCase() !== "POST") {
  throw new Error("telemetry ingest requires POST");
}

Try / catch

const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body });
if (res.status === 405) {
  // wrong verb; switch to POST
}
if (!res.ok) throw new Error(`ingest failed: ${res.status}`);

Prevention

When it happens

Trigger: Sending GET/PUT/DELETE/HEAD (or any method other than POST) to the ingest worker, including health checks or browser navigation to the ingest URL.

Common situations: Configuring a monitor or uptime checker that pings the endpoint with GET, a client using the wrong HTTP verb, or a user opening the ingest URL in a browser.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/a4fd1436813e7856. Report an issue: GitHub.

Appendix: source

Thrown at telemetry-ingest/src/index.ts:128

      chunks.push(value);
    }
  } finally {
    reader.releaseLock();
  }
  const joined = new Uint8Array(total);
  let offset = 0;
  for (const chunk of chunks) {
    joined.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return joined;
}

async function ingest(request: Request, env: Env): Promise<Response> {
  // Method before path, so a probe of any path with any verb other than POST
  // gets the same answer and learns nothing about what exists here.
  if (request.method !== "POST") {
    return status(405, { allow: "POST" });
  }
  if (new URL(request.url).pathname !== INGEST_PATH) {
    return status(404);
  }

  // Header read #1 of 2. `client.rs` sends exactly `application/json`.
  const contentType = request.headers.get("content-type") ?? "";
  if (!contentType.toLowerCase().startsWith("application/json")) {
    return status(415);
  }

  // Header read #2 of 2, and the last. See the red line above.
  const declared = request.headers.get("content-length");
  if (declared !== null) {
    const length = Number(declared);
    if (!Number.isFinite(length) || length > MAX_BODY_BYTES) {
      return status(413);
    }

View on GitHub (pinned to 433685b202)