fix: encode dynamic URL path segments (#5963)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
VectorPeak
2026-07-07 16:11:54 +05:30
committed by GitHub
co-authored by kartik-mem0
parent 87276ef968
commit cc52f0e367
9 changed files with 374 additions and 39 deletions
+12 -6
View File
@@ -67,6 +67,10 @@ function rejectTopLevelEntityParams(
}
}
function encodePathSegment(value: unknown): string {
return encodeURIComponent(String(value));
}
class APIError extends Error {
constructor(message: string) {
super(message);
@@ -311,7 +315,7 @@ export default class MemoryClient {
this._captureEvent("update", [payloadKeys]);
const response = await this._fetchWithErrorHandling(
`${this.host}/v1/memories/${memoryId}/`,
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/`,
{
method: "PUT",
headers: this.headers,
@@ -325,7 +329,7 @@ export default class MemoryClient {
if (this.telemetryId === "") await this.ping();
this._captureEvent("get", []);
return this._fetchWithErrorHandling(
`${this.host}/v1/memories/${memoryId}/`,
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/`,
{
headers: this.headers,
},
@@ -397,7 +401,7 @@ export default class MemoryClient {
// @ts-ignore
const query = new URLSearchParams(snakeOptions).toString();
return this._fetchWithErrorHandling(
`${this.host}/v1/memories/${memoryId}/${query ? `?${query}` : ""}`,
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/${query ? `?${query}` : ""}`,
{
method: "DELETE",
headers: this.headers,
@@ -428,7 +432,7 @@ export default class MemoryClient {
if (this.telemetryId === "") await this.ping();
this._captureEvent("history", []);
const response = await this._fetchWithErrorHandling(
`${this.host}/v1/memories/${memoryId}/history/`,
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/history/`,
{
headers: this.headers,
},
@@ -466,7 +470,7 @@ export default class MemoryClient {
data.entity_type = "user";
}
const response = await this._fetchWithErrorHandling(
`${this.host}/v1/entities/${data.entity_type}/${data.entity_id}/`,
`${this.host}/v1/entities/${encodePathSegment(data.entity_type)}/${encodePathSegment(data.entity_id)}/`,
{
method: "DELETE",
headers: this.headers,
@@ -510,7 +514,9 @@ export default class MemoryClient {
for (const entity of to_delete) {
try {
await this.client.delete(`/v2/entities/${entity.type}/${entity.name}/`);
await this.client.delete(
`/v2/entities/${encodePathSegment(entity.type)}/${encodePathSegment(entity.name)}/`,
);
} catch (error: any) {
throw new APIError(
`Failed to delete ${entity.type} ${entity.name}: ${error.message}`,
@@ -118,6 +118,20 @@ describe("MemoryClient - get()", () => {
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await expect(client.get("nonexistent")).rejects.toThrow("Memory not found");
});
test("URL-encodes memory ID path segments", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem%2Fa%3Fb%23c/", {
status: 200,
body: createMockMemory({ id: "mem/a?b#c" }),
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.get("mem/a?b#c");
expect(findFetchCall(mock, "/v1/memories/mem%2Fa%3Fb%23c/")).toBeDefined();
});
});
// ─── update() ────────────────────────────────────────────
@@ -213,6 +227,22 @@ describe("MemoryClient - update()", () => {
"At least one of text, metadata, timestamp, or expirationDate must be provided",
);
});
test("URL-encodes memory ID path segments", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem%2Fa%3Fb%23c/", {
status: 200,
body: createMockMemory({ id: "mem/a?b#c" }),
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.update("mem/a?b#c", { text: "Updated text" });
expect(
findFetchCall(mock, "/v1/memories/mem%2Fa%3Fb%23c/", "PUT"),
).toBeDefined();
});
});
// ─── delete() ────────────────────────────────────────────
@@ -250,6 +280,22 @@ describe("MemoryClient - delete()", () => {
expect(call).toBeDefined();
expect(call![0]).toContain("delete_linked=true");
});
test("URL-encodes memory ID path segments before adding query params", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem%2Fa%3Fb%23c/", {
status: 200,
body: { message: "Memory deleted successfully" },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.delete("mem/a?b#c", { deleteLinked: true });
const call = findFetchCall(mock, "/v1/memories/mem%2Fa%3Fb%23c/", "DELETE");
expect(call).toBeDefined();
expect(call![0]).toContain("delete_linked=true");
});
});
// ─── deleteAll() ─────────────────────────────────────────
@@ -326,4 +372,23 @@ describe("MemoryClient - history()", () => {
const result: MemoryHistory[] = await client.history("mem_123");
expect(result).toEqual([]);
});
test("URL-encodes memory ID path segments", async () => {
const historyEntries = [
createMockMemoryHistory({ memory_id: "mem/a?b#c" }),
];
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem%2Fa%3Fb%23c/history/", {
status: 200,
body: historyEntries,
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.history("mem/a?b#c");
expect(
findFetchCall(mock, "/v1/memories/mem%2Fa%3Fb%23c/history/"),
).toBeDefined();
});
});
@@ -71,4 +71,43 @@ describe("MemoryClient - deleteUser() (deprecated)", () => {
findFetchCall(mock, "/v1/entities/user/456/", "DELETE"),
).toBeDefined();
});
test("URL-encodes entity path segments", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/entities/team%2Ftype/org%2Fteam%3Factive%23frag/", {
status: 200,
body: { message: "Entity deleted successfully!" },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.deleteUser({
entity_id: "org/team?active#frag" as never,
entity_type: "team/type",
});
expect(
findFetchCall(
mock,
"/v1/entities/team%2Ftype/org%2Fteam%3Factive%23frag/",
"DELETE",
),
).toBeDefined();
});
});
describe("MemoryClient - deleteUsers()", () => {
test("URL-encodes entity path segments", async () => {
setupMockFetch();
const client = new MemoryClient({ apiKey: TEST_API_KEY });
client.telemetryId = "test@example.com";
const deleteMock = jest.fn().mockResolvedValue({});
client.client = { delete: deleteMock };
await client.deleteUsers({ userId: "org/team?active#frag" });
expect(deleteMock).toHaveBeenCalledWith(
"/v2/entities/user/org%2Fteam%3Factive%23frag/",
);
});
});