From 96e03dfe94050de673a6af79dbb97dab060d96c2 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Wed, 13 May 2026 21:00:34 +0200 Subject: [PATCH] handle axios header typing --- frontend/package-lock.json | 42 ++++++++++--- frontend/package.json | 2 +- .../src/core/contexts/FileManagerContext.tsx | 16 ++--- .../src/core/contexts/FilesModalContext.tsx | 32 ++++------ frontend/src/core/services/httpHeaderUtils.ts | 61 +++++++++++++++++++ .../core/services/signatureStorageService.ts | 5 +- .../proprietary/routes/ShareLinkLoader.tsx | 16 ++--- .../proprietary/services/shareLinkImport.ts | 12 +--- 8 files changed, 126 insertions(+), 60 deletions(-) create mode 100644 frontend/src/core/services/httpHeaderUtils.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd45f61e62..a165c7735f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -59,7 +59,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@userback/widget": "^0.3.12", "autoprefixer": "^10.4.21", - "axios": "^1.15.0", + "axios": "^1.15.2", "d3": "^7.9.0", "globals": "^17.5.0", "i18next": "^25.5.2", @@ -5903,16 +5903,42 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axios/node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -9197,9 +9223,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "engines": { diff --git a/frontend/package.json b/frontend/package.json index 18a89198e9..947ae32785 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,7 +55,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@userback/widget": "^0.3.12", "autoprefixer": "^10.4.21", - "axios": "^1.15.0", + "axios": "^1.15.2", "d3": "^7.9.0", "globals": "^17.5.0", "i18next": "^25.5.2", diff --git a/frontend/src/core/contexts/FileManagerContext.tsx b/frontend/src/core/contexts/FileManagerContext.tsx index 07bcd0924b..5b2f961346 100644 --- a/frontend/src/core/contexts/FileManagerContext.tsx +++ b/frontend/src/core/contexts/FileManagerContext.tsx @@ -22,6 +22,7 @@ import { extractLatestFilesFromBundle, parseContentDispositionFilename, } from "@app/services/shareBundleUtils"; +import { getHeaderValue } from "@app/services/httpHeaderUtils"; import { useTranslation } from "react-i18next"; import { openFilesFromDisk } from "@app/services/openFilesFromDisk"; export { pendingFilePathMappings } from "@app/services/pendingFilePathMappings"; @@ -893,16 +894,11 @@ export const FileManagerProvider: React.FC = ({ skipAuthRedirect: true, } as any, ); - const contentType = - (response.headers && - (response.headers["content-type"] || - response.headers["Content-Type"])) || - ""; - const disposition = - (response.headers && - (response.headers["content-disposition"] || - response.headers["Content-Disposition"])) || - ""; + const contentType = getHeaderValue(response.headers, "content-type"); + const disposition = getHeaderValue( + response.headers, + "content-disposition", + ); const filename = parseContentDispositionFilename(disposition) || file.name || diff --git a/frontend/src/core/contexts/FilesModalContext.tsx b/frontend/src/core/contexts/FilesModalContext.tsx index aec1915954..2c7703ea51 100644 --- a/frontend/src/core/contexts/FilesModalContext.tsx +++ b/frontend/src/core/contexts/FilesModalContext.tsx @@ -20,6 +20,7 @@ import { loadShareBundleEntries, parseContentDispositionFilename, } from "@app/services/shareBundleUtils"; +import { getHeaderValue } from "@app/services/httpHeaderUtils"; interface FilesModalContextType { isFilesModalOpen: boolean; @@ -173,16 +174,8 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ skipAuthRedirect: true, } as any, ); - const contentType = - (response.headers && - (response.headers["content-type"] || - response.headers["Content-Type"])) || - ""; - const disposition = - (response.headers && - (response.headers["content-disposition"] || - response.headers["Content-Disposition"])) || - ""; + const contentType = getHeaderValue(response.headers, "content-type"); + const disposition = getHeaderValue(response.headers, "content-disposition"); const filename = parseContentDispositionFilename(disposition) || "server-file"; const blob = response.data as Blob; @@ -199,16 +192,8 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ skipAuthRedirect: true, } as any, ); - const contentType = - (response.headers && - (response.headers["content-type"] || - response.headers["Content-Type"])) || - ""; - const disposition = - (response.headers && - (response.headers["content-disposition"] || - response.headers["Content-Disposition"])) || - ""; + const contentType = getHeaderValue(response.headers, "content-type"); + const disposition = getHeaderValue(response.headers, "content-disposition"); const filename = parseContentDispositionFilename(disposition) || "shared-file"; const blob = response.data as Blob; @@ -241,6 +226,11 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ // Use custom handler for special cases (like page insertion) customHandler(files, insertAfterPage); } else { + // Snapshot selection before upload; addFiles may replace the current + // selection with only newly uploaded files. + const previouslySelected = fileCtx.selectors + .getSelectedStirlingFileStubs() + .map((s) => s.id); // 1) Add via standard flow (auto-selects new files) await addFiles(files); // 2) Merge all requested file IDs (covers already-present files too) @@ -252,7 +242,7 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ .getSelectedStirlingFileStubs() .map((s) => s.id); const nextSelection = Array.from( - new Set([...currentSelected, ...ids]), + new Set([...previouslySelected, ...currentSelected, ...ids]), ); actions.setSelectedFiles(nextSelection); } diff --git a/frontend/src/core/services/httpHeaderUtils.ts b/frontend/src/core/services/httpHeaderUtils.ts new file mode 100644 index 0000000000..31a34ca9ac --- /dev/null +++ b/frontend/src/core/services/httpHeaderUtils.ts @@ -0,0 +1,61 @@ +type HeaderPrimitive = string | number | boolean; + +function normalizeHeaderValue(value: unknown): string | undefined { + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + if (Array.isArray(value)) { + const stringValues = value.filter( + (entry): entry is HeaderPrimitive => + typeof entry === "string" || + typeof entry === "number" || + typeof entry === "boolean", + ); + return stringValues.length > 0 + ? stringValues.map(String).join(", ") + : undefined; + } + return undefined; +} + +export function getHeaderValue(headers: unknown, headerName: string): string { + const normalizedName = headerName.toLowerCase(); + + if (!headers || typeof headers !== "object") { + return ""; + } + + const maybeHeaders = headers as Record & { + get?: (name: string) => unknown; + }; + + const directValue = + maybeHeaders[headerName] ?? + maybeHeaders[normalizedName] ?? + maybeHeaders[headerName.toUpperCase()]; + const normalizedDirect = normalizeHeaderValue(directValue); + if (normalizedDirect) { + return normalizedDirect; + } + + if (typeof maybeHeaders.get === "function") { + const fromGetter = normalizeHeaderValue(maybeHeaders.get(headerName)); + if (fromGetter) { + return fromGetter; + } + } + + for (const [key, value] of Object.entries(maybeHeaders)) { + if (key.toLowerCase() === normalizedName) { + const normalized = normalizeHeaderValue(value); + if (normalized) { + return normalized; + } + } + } + + return ""; +} diff --git a/frontend/src/core/services/signatureStorageService.ts b/frontend/src/core/services/signatureStorageService.ts index 018d39cfa7..93c418a0a6 100644 --- a/frontend/src/core/services/signatureStorageService.ts +++ b/frontend/src/core/services/signatureStorageService.ts @@ -1,4 +1,5 @@ import apiClient from "@app/services/apiClient"; +import { getHeaderValue } from "@app/services/httpHeaderUtils"; import type { SavedSignature } from "@app/types/signature"; export type StorageType = "backend" | "localStorage"; @@ -166,7 +167,9 @@ class SignatureStorageService { // Convert to data URL (base64) for both display and use const blob = new Blob([imageResponse.data], { - type: imageResponse.headers["content-type"] || "image/png", + type: + getHeaderValue(imageResponse.headers, "content-type") || + "image/png", }); const dataUrl = await new Promise((resolve, reject) => { diff --git a/frontend/src/proprietary/routes/ShareLinkLoader.tsx b/frontend/src/proprietary/routes/ShareLinkLoader.tsx index 827161d00c..0bb010fc10 100644 --- a/frontend/src/proprietary/routes/ShareLinkLoader.tsx +++ b/frontend/src/proprietary/routes/ShareLinkLoader.tsx @@ -16,6 +16,7 @@ import { loadShareBundleEntries, parseContentDispositionFilename, } from "@app/services/shareBundleUtils"; +import { getHeaderValue } from "@app/services/httpHeaderUtils"; interface ShareLinkLoaderProps { token: string; @@ -78,16 +79,11 @@ export default function ShareLinkLoader({ token }: ShareLinkLoaderProps) { ); if (signal.aborted) return; - const contentType = - (response.headers && - (response.headers["content-type"] || - response.headers["Content-Type"])) || - ""; - const disposition = - (response.headers && - (response.headers["content-disposition"] || - response.headers["Content-Disposition"])) || - ""; + const contentType = getHeaderValue(response.headers, "content-type"); + const disposition = getHeaderValue( + response.headers, + "content-disposition", + ); const filename = parseContentDispositionFilename(disposition) || "shared-file"; const blob = response.data as Blob; diff --git a/frontend/src/proprietary/services/shareLinkImport.ts b/frontend/src/proprietary/services/shareLinkImport.ts index 54860b1807..e58e1a9039 100644 --- a/frontend/src/proprietary/services/shareLinkImport.ts +++ b/frontend/src/proprietary/services/shareLinkImport.ts @@ -9,6 +9,7 @@ import { loadShareBundleEntries, parseContentDispositionFilename, } from "@app/services/shareBundleUtils"; +import { getHeaderValue } from "@app/services/httpHeaderUtils"; export interface ShareLinkMetadata { shareToken?: string; @@ -44,15 +45,8 @@ export async function downloadShareLink(token: string): Promise<{ suppressErrorToast: true, skipAuthRedirect: true, }); - const contentType = - (response.headers && - (response.headers["content-type"] || response.headers["Content-Type"])) || - ""; - const disposition = - (response.headers && - (response.headers["content-disposition"] || - response.headers["Content-Disposition"])) || - ""; + const contentType = getHeaderValue(response.headers, "content-type"); + const disposition = getHeaderValue(response.headers, "content-disposition"); const filename = parseContentDispositionFilename(disposition) || "shared-file"; const blob = response.data as Blob;