mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-02 22:38:18 +00:00
Preserve custom headers for no-store requests
This commit is contained in:
1 parent
a7274aafd6
commit
74d9f80761
2 files changed
+80
-9
No files matched your search
@@ -16,6 +16,43 @@ declare module "obsidian" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheReuseDirectives = new Set(["max-age", "max-stale", "min-fresh", "only-if-cached"]);
|
||||||
|
|
||||||
|
function splitHeaderDirectives(value: string): string[] {
|
||||||
|
const directives: string[] = [];
|
||||||
|
let start = 0;
|
||||||
|
let inQuotes = false;
|
||||||
|
let escaped = false;
|
||||||
|
|
||||||
|
for (let index = 0; index < value.length; index++) {
|
||||||
|
const character = value[index];
|
||||||
|
if (inQuotes && escaped) {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inQuotes && character === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === '"') {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
} else if (character === "," && !inQuotes) {
|
||||||
|
const directive = value.slice(start, index).trim();
|
||||||
|
if (directive) directives.push(directive);
|
||||||
|
start = index + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastDirective = value.slice(start).trim();
|
||||||
|
if (lastDirective) directives.push(lastDirective);
|
||||||
|
return directives;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirectiveName(directive: string): string {
|
||||||
|
const equalsIndex = directive.indexOf("=");
|
||||||
|
return (equalsIndex === -1 ? directive : directive.slice(0, equalsIndex)).trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceContext> {
|
export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceContext> {
|
||||||
_customHandler: ObsHttpHandler | undefined;
|
_customHandler: ObsHttpHandler | undefined;
|
||||||
_confirmInstance: Confirm;
|
_confirmInstance: Confirm;
|
||||||
@@ -185,13 +222,26 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
|
|||||||
const cacheMode = opts?.cache ?? (req instanceof Request ? req.cache : undefined);
|
const cacheMode = opts?.cache ?? (req instanceof Request ? req.cache : undefined);
|
||||||
if (cacheMode === "no-store" && ["GET", "HEAD"].includes(method.toUpperCase())) {
|
if (cacheMode === "no-store" && ["GET", "HEAD"].includes(method.toUpperCase())) {
|
||||||
// requestUrl has no cache option; no-store alone can reuse an existing cached response.
|
// requestUrl has no cache option; no-store alone can reuse an existing cached response.
|
||||||
for (const key of Object.keys(transformedHeaders)) {
|
const cacheControlKeys = Object.keys(transformedHeaders).filter(
|
||||||
if (["cache-control", "pragma"].includes(key.toLowerCase())) {
|
(key) => key.toLowerCase() === "cache-control"
|
||||||
delete transformedHeaders[key];
|
);
|
||||||
}
|
const cacheControlDirectives = cacheControlKeys
|
||||||
}
|
.flatMap((key) => splitHeaderDirectives(transformedHeaders[key]))
|
||||||
transformedHeaders["cache-control"] = "no-cache, no-store";
|
.filter((directive) => {
|
||||||
transformedHeaders["pragma"] = "no-cache";
|
const name = getDirectiveName(directive);
|
||||||
|
return !cacheReuseDirectives.has(name) && name !== "no-cache" && name !== "no-store";
|
||||||
|
});
|
||||||
|
const cacheControlKey = cacheControlKeys[0] ?? "cache-control";
|
||||||
|
for (const key of cacheControlKeys) delete transformedHeaders[key];
|
||||||
|
transformedHeaders[cacheControlKey] = [...cacheControlDirectives, "no-cache", "no-store"].join(", ");
|
||||||
|
|
||||||
|
const pragmaKeys = Object.keys(transformedHeaders).filter((key) => key.toLowerCase() === "pragma");
|
||||||
|
const pragmaDirectives = pragmaKeys
|
||||||
|
.flatMap((key) => splitHeaderDirectives(transformedHeaders[key]))
|
||||||
|
.filter((directive) => getDirectiveName(directive) !== "no-cache");
|
||||||
|
const pragmaKey = pragmaKeys[0] ?? "pragma";
|
||||||
|
for (const key of pragmaKeys) delete transformedHeaders[key];
|
||||||
|
transformedHeaders[pragmaKey] = [...pragmaDirectives, "no-cache"].join(", ");
|
||||||
}
|
}
|
||||||
const contentType =
|
const contentType =
|
||||||
transformedHeaders["content-type"] ?? transformedHeaders["Content-Type"] ?? "application/json";
|
transformedHeaders["content-type"] ?? transformedHeaders["Content-Type"] ?? "application/json";
|
||||||
|
|||||||
@@ -94,13 +94,34 @@ describe("ObsidianAPIService.nativeFetch cache control", () => {
|
|||||||
|
|
||||||
await createService({}).nativeFetch(url, { method, cache: "no-store", headers });
|
await createService({}).nativeFetch(url, { method, cache: "no-store", headers });
|
||||||
|
|
||||||
expect(sentHeaders().get("cache-control")).toBe("no-cache, no-store");
|
expect(sentHeaders().get("cache-control")).toBe("public, no-cache, no-store");
|
||||||
expect(sentHeaders().get("pragma")).toBe("no-cache");
|
expect(sentHeaders().get("pragma")).toBe("cache, no-cache");
|
||||||
expect(sentHeaders().get("authorization")).toBe("Basic fixture");
|
expect(sentHeaders().get("authorization")).toBe("Basic fixture");
|
||||||
expect(headers["Cache-Control"]).toBe("public, max-age=3600");
|
expect(headers["Cache-Control"]).toBe("public, max-age=3600");
|
||||||
expect(mocks.requestUrl.mock.calls[0][0]).toMatchObject({ url, method });
|
expect(mocks.requestUrl.mock.calls[0][0]).toMatchObject({ url, method });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves non-conflicting cache directives when enforcing no-store", async () => {
|
||||||
|
const headers = {
|
||||||
|
"Cache-Control": 'max-age=3600, no-transform, custom="a,b"',
|
||||||
|
"cAcHe-CoNtRoL": 'max-stale=30, min-fresh=5, only-if-cached, custom-two="max-age=1, only-if-cached"',
|
||||||
|
Pragma: 'legacy="a,b", no-cache',
|
||||||
|
"X-Custom-Header": "preserved",
|
||||||
|
};
|
||||||
|
const originalHeaders = { ...headers };
|
||||||
|
|
||||||
|
await createService({}).nativeFetch(url, { cache: "no-store", headers });
|
||||||
|
|
||||||
|
const sent = mocks.requestUrl.mock.calls[0][0].headers ?? {};
|
||||||
|
expect(sent["Cache-Control"]).toBe(
|
||||||
|
'no-transform, custom="a,b", custom-two="max-age=1, only-if-cached", no-cache, no-store'
|
||||||
|
);
|
||||||
|
expect(sent["cAcHe-CoNtRoL"]).toBeUndefined();
|
||||||
|
expect(sent.Pragma).toBe('legacy="a,b", no-cache');
|
||||||
|
expect(sent["X-Custom-Header"]).toBe("preserved");
|
||||||
|
expect(headers).toEqual(originalHeaders);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses the Request cache mode when the options do not override it", async () => {
|
it("uses the Request cache mode when the options do not override it", async () => {
|
||||||
const request = new Request(url, {
|
const request = new Request(url, {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
|
|||||||
Reference in new issue
Block a user