Skip to content

rest: GET /data/OBJ/export silently answers an unknown format (pdf, xml, empty) with a 200 CSV instead of refusing it #27500

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcSwDXZrZg7nGjP6P3U

Filing gate: ① a product defect with a reproduction, found by an off-clause probe of QA run #27438 (wave 3 of the 18.3.0 sweep, anchor #27377). It was reproduced with 3 spellings. reach: measured at the public export door.
Business reason: an integration or script that asks for format=pdf, xml or a typo gets a 200 with a CSV attachment and no signal that the request was not honoured. Downstream code that branches on the status, or saves the file under the requested extension, silently mishandles it. The other format-bearing doors refuse unknown values.
Reader who acts: the triage seat, then the implementer for packages/rest (src/data-action-routes.ts, the export route).
Dedupe (open AND closed): the query "export unknown format pdf returns csv 200 instead of 400" returned 0 hits.

Batch-of: 16 · QA run #27438

Symptom

GET /data/OBJ/export silently coerces an unknown or empty format to CSV instead of refusing it.

Reproduction (subject c4c515a, stock showcase)

  1. As a caller holding the export grant, GET /api/v1/data/showcase_task/export?format=pdf → 200, text/csv, a .csv attachment, X-Export-Format: csv.
  2. The same happens with format=xml and with format= (empty).
  3. The supported values csv, xlsx and json each answer their own format.

Expected: an unknown format answers 400, naming the value and the supported set. An absent format may still default to CSV.

Mechanism

packages/rest/src/data-action-routes.ts lines 1074–1076 map any value other than json or xlsx to CSV.

QA-source: #27438 · records-forms.list-view-capabilities · observed beside the clauses


Generated by Claude Code

Activity

  1. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Oct 29, 2026
  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 可对外的 API | records-forms.list-view-capabilities | P②

    Triage: p2 · domain:server · area:api · 2026-10-29T11:22Z

    ⛔ Not a claim.

    • Read fresh. GET /data/OBJ/export silently answers an unknown or empty format (pdf, xml) with a 200 CSV, where other format-bearing doors refuse. Silent coercion of the caller's request: p2.
    • An unknown format answers 400 naming the value and the supported set, and an absent format still defaults to CSV. This is one format table in data-action-routes.
    • bug, pm:queue.

    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 29, 2026
  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    Claim: PM loop round 1 · fold chain head (#27500 + #27489)
    Session: session_01L6bhgUCizoYWQVJWjhdR6F
    Account: os-sales
    Branch: claude/issue-27500-rest-door-refuse-malformed
    Worktree: objectstack-issue-27500
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/rest/src/data-action-routes.ts (the export handler's format parse; the clone handler's overrides read) and its tests (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: packages/rest data door (this lane) | no platform path refuses these values today | any API caller of export / clone
    Thread-read: 6495304429
    Premise: holds (read on main 2026-10-29T13:18Z: the export handler maps any format other than json/xlsx to csv; the clone handler drops a non-object overrides and lets an array through to cloneData)
    Serial constraints cleared: none (no open PR edits packages/rest/src/data-action-routes.ts; #27501 on the same file is held until this lands)

    Fold (five gates): ① same defect shape and fix — a malformed request value on a /data door is coerced silently; the fix is a 400 that names the parameter and its accepted set; ② one file, one worktree, one changeset; ③ both triaged, neither in the decision box; ④ each has its own named pin (format=pdf → 400; overrides: "x" / [] → 400); ⑤ excluded: #27501 (existence check, a different shape), #27484 (contradicts the pinned "empty override is ABSENT" posture). The one PR carries Fixes #27500 and Fixes #27489, one commit per card.

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
    os-dev-report

    {
      "issue": 27500,
      "folded": [
        27489
      ],
      "status": "done",
      "branch": "claude/issue-27500-rest-door-refuse-malformed",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27659",
      "head": "035ecdcf0a8a46b4f47bdb7cd4b3ca12a0abb735",
      "session": "session_01L6bhgUCizoYWQVJWjhdR6F",
      "premise_still_valid": true,
      "summary": "Both premises held on main 72b08eec. #27500: GET /data/:object/export now reads format through readExportFormat. Absent is csv. A served value (csv, json or xlsx, trimmed and case-insensitive, the same reading template mode gives format) is itself. Anything else answers 400 VALIDATION_ERROR naming the value and the set, before findData. format= (empty) is refused, the same reading limit= already gets. #27489: POST /data/:object/:id/clone refuses a nested overrides or a bare body that is not a JSON object (string, number, boolean, null or array) with 400 VALIDATION_ERROR naming overrides, before cloneData. \"overrides\": null is refused, not read as absent. A bare null body or no body is still a plain copy. Both refusals use the shared nested writer (sendError, @objectstack/types) and an existing StandardErrorCode. No spec, metadata-protocol or docs edit. The assignee field was not read or written.",
      "tests": "New suites through the real HonoHttpServer: export-format-refusal.test.ts and clone-overrides-refusal.test.ts, Test Files 2 passed, Tests 28 passed. Each refusal asserts status 400, error.code VALIDATION_ERROR, the parameter named, and the service (findData or cloneData) never called. Each preservation case asserts the served answer or the overrides cloneData received. Full package at 035ecdcf, under the lock: rest build, test and typecheck gave VERDICT command-exit 0, Test Files 337 passed, Tests 6615 passed and 326 skipped, tsc clean, check:test-typecheck OK with 0 debt. Ablation via scripts/ablation-replace.mjs in wrap mode with a trap; tests import src, so there is no dist leg. (1) Old csv fallback put back in readExportFormat: anchor 1 to 0, blob 4a4b352a to 39d51ee7, 5 failed and 8 passed ('expected 200 to be 400'), restored blob == HEAD 4a4b352a and git diff HEAD empty. (2) Clone guard removed and the old typeof pass-through put back: anchor 1 to 0, blob 4a4b352a to 8491a123, 9 failed and 6 passed ('expected 201 to be 400'), restored blob == HEAD and diff empty.",
      "gates": "Local, at 035ecdcf: dispatch-gates --commands derived 59 commands, all exit 0 (captured before any pipe). --ran gave '59 derived, 59 run, 0 NOT-MEASURED (a DERIVED zero, all 59 recorded an exit code)'. Pre-merge, check:dual-build-cjs-loads (no dist) and check-plugin-teardown-shape --self-test (shallow) answered exit 3; both were 0 on the final run. eslint --no-inline-config packages/rest: exit 0, pre-merge and at final head. Control-byte scan of the changed files: 0 hits. Open PRs on the same files: 0 of 43. CI at 035ecdcf, snapshot at report time: 21 check-runs, 3 skipped (opt-in or schedule) and 18 in_progress, among them Lint ESLint, Type Check workspace, Check Changeset and Governed Surface Queue Guard. Not waited on, per contract. test:repo is CI's.",
      "dogfood_battery": "035ecdcf · VERDICT command-exit 1 (held 3349s) · Test Files 2 failed, 349 passed, 3 skipped (354) · Tests 2 failed, 2820 passed, 20 skipped (2842). Failures: object-designer-field-reorder.dogfood.test.ts 'creates and publishes the object in its declared order' and wall-package-refusal.dogfood.test.ts (the operator CONTROL install, upgrade and uninstall round trip), both 'Test timed out in 5000ms'. Neither calls the data export or clone door. Re-run of the two files alone: the same 2 timed out (about 5.4s, box shared with another worktree's turbo build). With --testTimeout=30000: 2 files and 24 tests passed. Read: box load, not this diff.",
      "line_budget": "296 changed lines (+288/-8) over 4 files, against a human-merge threshold of 3000: under",
      "files_changed": [
        "packages/rest/src/data-action-routes.ts",
        "packages/rest/src/export-format-refusal.test.ts",
        "packages/rest/src/clone-overrides-refusal.test.ts",
        ".changeset/27500-27489-rest-door-refuse-malformed.md"
      ],
      "deviations": [
        "Clause-② arm: the claims say 'Clause-②: no'. The PR body and changeset say 'Clause-②: no (narrowing)', with a BREAKING banner, the migration and the ADR-0087 marker not-required (no-migration-prescription), at patch level. Both doors now refuse values they accepted, which AGENTS.md step 3 classes as narrowing, and the dispatch asked for this decision. Seat to confirm.",
        "Commit shape: two card commits, then 'test(rest): type the clone refusal test's parsed body' (an as-any cast tsconfig.test.json needed), then the changeset commit, then one merge of origin/main. Folding the cast into the #27489 commit with --force-with-lease was refused by this session's auto-mode classifier. It was not retried another way.",
        "Lock narrowing, declared: the first run of the two new suites and the two-file dogfood re-run ran without the verify lock, after 27 to 30 minutes of queue-timeouts behind other worktrees' holders. The closure build, full rest test and typecheck, and the dogfood battery all ran under the lock.",
        "Writes: four in total, against the three-write budget in os-dev.md: git push, relay pr_create, the report on #27500, and a pointer comment on #27489. The dispatch asked for the pointer."
      ],
      "mcp_calls": "0",
      "api_writes": "3: (1) relay pr_create, repository_dispatch, run 40913618238, POST /repos/objectstack-ai/objectstack/pulls draft with no assignees or reviewers, giving #27659; (2) post-stamped comment POST /repos/objectstack-ai/objectstack/issues/27500/comments (this report); (3) post-stamped comment POST /repos/objectstack-ai/objectstack/issues/27489/comments (pointer). git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: a clone body carrying overrides plus other top-level keys reads only overrides and ignores the rest (unchanged by this PR). Not measured at the door, so it stays in Acceptance notes. carrier: none. Dedupe words: clone overrides sibling keys ignored, clone bare map mixed body"
      ]
    }

    Posted 2026-10-29T16:40Z.

  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

    ACCEPT → PR #27659 comment 6503112499 · head 035ecdcf · 2026-10-29T17:00Z · landing sent (contract review PASS 6503044025).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions