From 638f432f2cc854fcb79465054e78aec488870bc0 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:51:30 -0500 Subject: [PATCH 1/2] fix fmodata accept header override _makeRequestEffect overwrote caller Accept; getMetadata({format:"xml"}) got json back, mis-cast to string, broke typegen parseMetadata. Co-Authored-By: Claude Opus 4.7 --- .changeset/fix-fmodata-respect-caller-accept-header.md | 5 +++++ packages/fmodata/src/client/filemaker-odata.ts | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-fmodata-respect-caller-accept-header.md diff --git a/.changeset/fix-fmodata-respect-caller-accept-header.md b/.changeset/fix-fmodata-respect-caller-accept-header.md new file mode 100644 index 00000000..e0968b8d --- /dev/null +++ b/.changeset/fix-fmodata-respect-caller-accept-header.md @@ -0,0 +1,5 @@ +--- +"@proofkit/fmodata": patch +--- + +Fix `_makeRequestEffect` unconditionally overwriting the caller-supplied `Accept` header. `getMetadata({ format: "xml" })` was setting `Accept: application/xml` which got clobbered with `application/json`, causing the server to return JSON metadata that was then mis-cast to a string and handed to fast-xml-parser. Now the default Accept is only applied when the caller hasn't specified one. This unblocks `@proofkit/typegen` for fmodata configs. diff --git a/packages/fmodata/src/client/filemaker-odata.ts b/packages/fmodata/src/client/filemaker-odata.ts index f0c38385..4c701aa4 100644 --- a/packages/fmodata/src/client/filemaker-odata.ts +++ b/packages/fmodata/src/client/filemaker-odata.ts @@ -252,7 +252,12 @@ export class FMServerConnection implements ExecutionContext { const headers = new Headers(options?.headers); headers.set("Authorization", await this._getAuthorizationHeader(fetchHandler)); headers.set("Content-Type", "application/json"); - headers.set("Accept", getAcceptHeader(includeODataAnnotations)); + // Respect a caller-supplied Accept header (e.g. getMetadata({ format: "xml" }) + // sets Accept: application/xml). Only fall back to the default JSON Accept + // when the caller didn't specify one. + if (!headers.has("Accept")) { + headers.set("Accept", getAcceptHeader(includeODataAnnotations)); + } const mergedPrefer = mergePreferHeaderValues( preferValues.length > 0 ? preferValues.join(", ") : undefined, From c031d74ec5c512cd31358ea8acf97c7dd0fc9655 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:51:38 -0500 Subject: [PATCH 2/2] improve typegen metadata parse error Detect empty/JSON/HTML responses and include 500-char excerpt instead of opaque "No Edmx element found in XML". Co-Authored-By: Claude Opus 4.7 --- .changeset/improve-typegen-metadata-error.md | 5 +++ packages/typegen/src/fmodata/parseMetadata.ts | 39 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 .changeset/improve-typegen-metadata-error.md diff --git a/.changeset/improve-typegen-metadata-error.md b/.changeset/improve-typegen-metadata-error.md new file mode 100644 index 00000000..0e529822 --- /dev/null +++ b/.changeset/improve-typegen-metadata-error.md @@ -0,0 +1,5 @@ +--- +"@proofkit/typegen": patch +--- + +Improve `parseMetadata` error messages: when the OData metadata response is missing ``, surface a response excerpt and recognize common failure modes (empty body, JSON error payload, HTML login redirect) instead of throwing the opaque "No Edmx element found in XML". diff --git a/packages/typegen/src/fmodata/parseMetadata.ts b/packages/typegen/src/fmodata/parseMetadata.ts index 93f747c4..dfc304d5 100644 --- a/packages/typegen/src/fmodata/parseMetadata.ts +++ b/packages/typegen/src/fmodata/parseMetadata.ts @@ -51,6 +51,36 @@ function ensureArray(value: T | T[] | undefined): T[] { return Array.isArray(value) ? value : [value]; } +const RESPONSE_EXCERPT_LIMIT = 500; + +/** + * Builds a diagnostic error message when the metadata response is missing the + * expected `edmx:Edmx` root. Tries to recognize common failure modes (empty + * body, JSON error payload, HTML login page) and always includes a short + * excerpt of what was actually received so the cause is debuggable. + */ +function describeNonEdmxResponse(xmlString: string): string { + const trimmed = xmlString.trim(); + if (trimmed.length === 0) { + return "OData metadata response was empty. Verify the server URL, database name, and credentials."; + } + + const excerpt = trimmed.slice(0, RESPONSE_EXCERPT_LIMIT); + const truncated = trimmed.length > RESPONSE_EXCERPT_LIMIT ? "…" : ""; + const contextSuffix = ` First ${Math.min(trimmed.length, RESPONSE_EXCERPT_LIMIT)} chars of response:\n${excerpt}${truncated}`; + + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + return `OData metadata endpoint returned JSON instead of XML. This usually means the server (or the fmodata client) responded to the request as JSON. Check that the request was made with Accept: application/xml.${contextSuffix}`; + } + + const lower = trimmed.slice(0, 200).toLowerCase(); + if (lower.startsWith("(); let namespace = ""; + // Defensive: callers sometimes hand us non-string payloads (e.g. an object + // resulting from a JSON-typed response misrouted as XML). Stringify so the + // diagnostic excerpt below is meaningful instead of "[object Object]". + const xmlString = typeof xmlContent === "string" ? xmlContent : JSON.stringify(xmlContent); + // Parse XML using fast-xml-parser const parser = new XMLParser({ ignoreAttributes: false, @@ -71,12 +106,12 @@ export function parseMetadata(xmlContent: string): ParsedMetadata { trimValues: true, }); - const parsed = parser.parse(xmlContent); + const parsed = parser.parse(xmlString); // Navigate to Schema element const edmx = parsed["edmx:Edmx"] || parsed.Edmx; if (!edmx) { - throw new Error("No Edmx element found in XML"); + throw new Error(describeNonEdmxResponse(xmlString)); } const dataServices = edmx["edmx:DataServices"] || edmx.DataServices;