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/.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/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, 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;