Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-fmodata-respect-caller-accept-header.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/improve-typegen-metadata-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/typegen": patch
---

Improve `parseMetadata` error messages: when the OData metadata response is missing `<edmx:Edmx>`, 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".
7 changes: 6 additions & 1 deletion packages/fmodata/src/client/filemaker-odata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
39 changes: 37 additions & 2 deletions packages/typegen/src/fmodata/parseMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,36 @@ function ensureArray<T>(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("<!doctype html") || lower.startsWith("<html")) {
return `OData metadata endpoint returned an HTML page instead of XML. The server may be redirecting to a login or error page.${contextSuffix}`;
}

return `No Edmx element found in OData metadata XML.${contextSuffix}`;
}

/**
* Parses OData metadata XML content and extracts entity types, entity sets, and namespace.
*
Expand All @@ -62,6 +92,11 @@ export function parseMetadata(xmlContent: string): ParsedMetadata {
const entitySets = new Map<string, EntitySet>();
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,
Expand All @@ -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;
Expand Down
Loading