# Design — add-export-download-handler ## Goal Close the deferred `GET /:id/download` endpoint without expanding scope into §4 streaming / S3 / signed-URL work. The handler reads the persisted job, re-runs the registered `DataProvider`, re-renders through the existing writer, and streams the result back. Errors flow through a per-package RFC 7807 problem-details renderer so consumers branch on the existing `EXPORT.*` codes. ## File layout ``` packages/api/api-plugin-export/src/ error-codes/ error-codes.ts ← add JOB_NOT_READY otel/ instruments.ts ← add downloadServed / downloadFailed routes/ download/ download.ts ← runDownloadExport(deps, jobId) download.md download.test.ts exports/ exports.ts ← extend deps + mount /:id/download problem-json/ problem-json.ts ← RFC 7807 renderer (per-package) problem-json.md problem-json.test.ts plugin/ plugin.ts ← thread exportJobStore through to routes ``` ## Discriminated outcome `runDownloadExport(...)` returns a union so the route registration call site stays free of `c.json(...)` typing. The four failure variants each carry the data the renderer needs: ```ts type DownloadOutcome = | { kind: "ok"; body: string; contentType: string; filename: string; format: ExportFormat } | { kind: "not-found"; id: string } | { kind: "not-ready"; id: string; status: ExportStatus } | { kind: "provider-not-found"; resource: string } | { kind: "failed"; reason: string; cause: unknown }; ``` Every branch throws / instantiates an `ExportError` so the spec-conformance pin keeps every `EXPORT_*` code reachable from a production path. ## `jobStore` injection vs the in-memory `jobs` Map `ExportRoutesDeps.jobStore?` is added as an optional dependency. When absent, the download handler builds an inline `ExportJobStore` adapter over `deps.jobs` so the route works in lightweight tests and host apps that opt out of SurrealDB. When present, the handler routes all reads through the store — consistent with the persistence narrative in `2026-05-19-wire-surreal-export-store`. ## OTel labels - `export.download.served{format}` — labelled with `format: csv | json | xlsx`. - `export.download.failed{reason}` — labelled with `reason: not-found | not-ready | provider-not-found | failed`. Tests assert via `flushTestMetrics()` against the existing in-memory `MeterProvider` in `tests/setup/polyfills.ts`. ## Status-code mapping | Outcome | HTTP | Code | |---|---|---| | `ok` | 200 | n/a | | `not-found` | 404 | `EXPORT.JOB_NOT_FOUND` | | `not-ready` | 409 | `EXPORT.JOB_NOT_READY` | | `provider-not-found` | 404 | `EXPORT.PROVIDER_NOT_FOUND` | | `failed` | 502 | `EXPORT.PROVIDER_FETCH_FAILED` | The mapping lives in the per-package `problem-json` renderer via `context.statusCode` attached at throw time.