Skip to content

Commit b075656

Browse files
author
Chris Corsi
committed
fix(fmodata): strip otto prefix and .fmp12 from batch sub-request URLs
Batch sub-request URLs inside the multipart body are processed directly by FileMaker's OData engine, not through the Otto proxy. The engine rejects URLs containing the /otto/ prefix (-1000 error) or the .fmp12 extension (-1032 error). Add toBatchSubRequestUrl() to transform full URLs into the canonical /fmi/odata/v4/{database}/{table} format before writing them into the batch body. Closes #207
1 parent 06173e6 commit b075656

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@proofkit/fmodata": patch
3+
---
4+
5+
Fix batch sub-request URLs to use canonical FileMaker OData path format. Strips the Otto proxy prefix (`/otto/`) and `.fmp12` file extension from database names in sub-request URLs inside multipart batch bodies, which are processed directly by FileMaker's OData engine.

packages/fmodata/src/client/batch-request.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const BOUNDARY_REGEX = /boundary=([^;]+)/;
1010
const HTTP_STATUS_LINE_REGEX = /HTTP\/\d\.\d\s+(\d+)\s*(.*)/;
1111
const CRLF_REGEX = /\r\n/;
1212
const CHANGESET_CONTENT_TYPE_REGEX = /Content-Type: multipart\/mixed;\s*boundary=([^\r\n]+)/;
13+
const OTTO_PREFIX_REGEX = /^\/otto/;
14+
const FMPRO_EXT_REGEX = /\.fmp12/;
1315

1416
export interface RequestConfig {
1517
method: string;
@@ -62,6 +64,18 @@ async function requestToConfig(request: Request): Promise<RequestConfig> {
6264
};
6365
}
6466

67+
/**
68+
* Transforms a full URL into the canonical path format required by FileMaker's
69+
* OData batch processor. Strips proxy prefixes (e.g. /otto/) and the .fmp12
70+
* file extension from the database name segment.
71+
*/
72+
export function toBatchSubRequestUrl(fullUrl: string): string {
73+
const url = new URL(fullUrl);
74+
const path = url.pathname.replace(OTTO_PREFIX_REGEX, "");
75+
const batchPath = path.replace(FMPRO_EXT_REGEX, "");
76+
return `${batchPath}${url.search}`;
77+
}
78+
6579
/**
6680
* Formats a single HTTP request for inclusion in a batch
6781
* @param request - The request configuration
@@ -80,11 +94,15 @@ function formatSubRequest(request: RequestConfig, baseUrl: string): string {
8094
lines.push("Content-Transfer-Encoding: binary");
8195
lines.push(""); // Empty line after multipart headers
8296

83-
// Construct full URL (convert relative to absolute)
97+
// Construct sub-request URL as a canonical FileMaker OData path.
98+
// Sub-requests inside the batch body are processed directly by FileMaker's
99+
// OData engine, so they must not include proxy prefixes (e.g. /otto/) or
100+
// the .fmp12 file extension on the database name.
84101
const fullUrl = request.url.startsWith("http") ? request.url : `${baseUrl}${request.url}`;
102+
const subRequestUrl = toBatchSubRequestUrl(fullUrl);
85103

86104
// Add HTTP request line
87-
lines.push(`${request.method} ${fullUrl} HTTP/1.1`);
105+
lines.push(`${request.method} ${subRequestUrl} HTTP/1.1`);
88106

89107
// For requests with body, add headers
90108
if (request.body) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, expect, it } from "vitest";
2+
import { formatBatchRequest, toBatchSubRequestUrl } from "../src/client/batch-request";
3+
4+
describe("toBatchSubRequestUrl", () => {
5+
it("strips /otto/ prefix and .fmp12 extension", () => {
6+
const result = toBatchSubRequestUrl(
7+
"https://host.example.com/otto/fmi/odata/v4/GMT_Web.fmp12/bookings?$top=1&$select=_GMTNum",
8+
);
9+
expect(result).toBe("/fmi/odata/v4/GMT_Web/bookings?$top=1&$select=_GMTNum");
10+
});
11+
12+
it("strips .fmp12 extension without /otto/ prefix", () => {
13+
const result = toBatchSubRequestUrl("https://host.example.com/fmi/odata/v4/GMT_Web.fmp12/bookings");
14+
expect(result).toBe("/fmi/odata/v4/GMT_Web/bookings");
15+
});
16+
17+
it("handles URLs without /otto/ or .fmp12", () => {
18+
const result = toBatchSubRequestUrl("https://host.example.com/fmi/odata/v4/MyDB/contacts");
19+
expect(result).toBe("/fmi/odata/v4/MyDB/contacts");
20+
});
21+
22+
it("preserves query parameters", () => {
23+
const result = toBatchSubRequestUrl(
24+
"https://host.example.com/otto/fmi/odata/v4/MyDB.fmp12/contacts?$filter=name eq 'test'&$top=10",
25+
);
26+
expect(result).toBe("/fmi/odata/v4/MyDB/contacts?$filter=name%20eq%20%27test%27&$top=10");
27+
});
28+
});
29+
30+
describe("formatBatchRequest sub-request URLs", () => {
31+
it("uses canonical paths without /otto/ prefix or .fmp12 in sub-requests", () => {
32+
const baseUrl = "https://host.example.com/otto/fmi/odata/v4/GMT_Web.fmp12";
33+
const { body } = formatBatchRequest(
34+
[{ method: "GET", url: `${baseUrl}/bookings?$top=1&$select=_GMTNum` }],
35+
baseUrl,
36+
);
37+
38+
// The sub-request line must use the canonical path
39+
expect(body).toContain("GET /fmi/odata/v4/GMT_Web/bookings?$top=1&$select=_GMTNum HTTP/1.1");
40+
// Must NOT contain the otto prefix or .fmp12 in the request line
41+
expect(body).not.toContain("/otto/");
42+
expect(body).not.toContain(".fmp12");
43+
});
44+
45+
it("handles relative URLs by prepending baseUrl then transforming", () => {
46+
const baseUrl = "https://host.example.com/otto/fmi/odata/v4/MyDB.fmp12";
47+
const { body } = formatBatchRequest([{ method: "GET", url: "/contacts?$top=5" }], baseUrl);
48+
49+
expect(body).toContain("GET /fmi/odata/v4/MyDB/contacts?$top=5 HTTP/1.1");
50+
});
51+
});

0 commit comments

Comments
 (0)