From e78437cb2a9cfb2789b5f9ff77739b1f3d0c3c25 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 16 Sep 2026 00:18:31 +0800 Subject: [PATCH] fix(web): upgrade http OSS URLs to https to fix mixed-content block Aliyun OSS sometimes returns presigned URLs as http:// even though every page here is served over https:// -- the browser silently blocks these as mixed content, which is why images (e.g. avatar) failed to display after upload. Added upgradeToHttps() and wired it into fileDownloadUrl() (the single chokepoint every image/attachment fetch in the app resolves through) plus the three chat-attachment send paths, so both freshly fetched and newly-persisted URLs get the scheme fix. Also fixed ProfileSettingsPage's avatar upload handler, which extracted the file id by parsing the upload response's `url` with .split('/').pop() instead of just reading the `file_id` field the response already provides -- fragile against exactly this kind of full-URL response and the most likely actual trigger for the reported bug. vite.config.ts: dev server port now respects $PORT if set, falls back to 8094 -- unrelated cleanup, needed it to run a second local dev server for verification alongside another concurrent session's. Co-Authored-By: Claude Sonnet 5 --- web/src/features/settings/ProfileSettingsPage.tsx | 3 +-- web/src/lib/api/upload.ts | 12 +++++++++++- web/src/lib/tinode/attachments.ts | 8 ++++---- web/vite.config.ts | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/web/src/features/settings/ProfileSettingsPage.tsx b/web/src/features/settings/ProfileSettingsPage.tsx index 0958b144..3852fc7b 100644 --- a/web/src/features/settings/ProfileSettingsPage.tsx +++ b/web/src/features/settings/ProfileSettingsPage.tsx @@ -85,9 +85,8 @@ export function ProfileSettingsPage() { const avatarMutation = useMutation({ mutationFn: async (file: File) => { const uploaded = await uploadFile(file); - const fileId = uploaded.url.split('/').pop() || ''; if (!query.data) throw new Error('尚未加载'); - return updateMyProfile({ avatar_file_id: fileId, version: query.data.version }); + return updateMyProfile({ avatar_file_id: uploaded.file_id, version: query.data.version }); }, onSuccess: (data) => { queryClient.setQueryData(['my-profile'], (prev: typeof query.data) => (prev ? { ...prev, ...data } : data)); diff --git a/web/src/lib/api/upload.ts b/web/src/lib/api/upload.ts index 73a4d1da..4790fdad 100644 --- a/web/src/lib/api/upload.ts +++ b/web/src/lib/api/upload.ts @@ -72,6 +72,16 @@ export function fileAuthHeaders(): Record { return { 'X-MeetingIM-APIKey': API_KEY, Authorization: `Token ${authToken()}` }; } +/** + * Some backend-issued absolute URLs (e.g. Aliyun OSS presigned URLs) come back as `http://` even + * though every page in this app is served over `https://` -- the browser blocks those as mixed + * content before any of our code sees a response. There's no legitimate case where this app needs + * to fetch a plain-http resource, so upgrading the scheme here is safe and has no downside. + */ +export function upgradeToHttps(url: string): string { + return url.startsWith('http://') ? `https://${url.slice('http://'.length)}` : url; +} + /** * Absolute download URL for a file reference returned by uploadFile() or a received attachment. * The upload response's `url` (e.g. "/v0/file/s/file123") is already rooted at the tenant's file @@ -79,7 +89,7 @@ export function fileAuthHeaders(): Record { * fileBase() down to just its origin before appending, rather than concatenating the two paths. */ export function fileDownloadUrl(refUrl: string): string { - if (refUrl.startsWith('http')) return refUrl; + if (refUrl.startsWith('http')) return upgradeToHttps(refUrl); const origin = fileBase().replace(/\/v0\/file.*$/, ''); return `${origin}${refUrl}`; } diff --git a/web/src/lib/tinode/attachments.ts b/web/src/lib/tinode/attachments.ts index aa912e16..07f991b2 100644 --- a/web/src/lib/tinode/attachments.ts +++ b/web/src/lib/tinode/attachments.ts @@ -1,6 +1,6 @@ import { Drafty, type Topic } from 'tinode-sdk'; -import { uploadFile } from '@/lib/api/upload'; +import { uploadFile, upgradeToHttps } from '@/lib/api/upload'; import { MAX_INBAND_ATTACHMENT_SIZE } from '@/lib/config'; import { newClientMid } from '@/lib/tinode/send'; @@ -43,7 +43,7 @@ export async function sendImage(topic: Topic, file: File): Promise { ? Drafty.insertImage(undefined, 0, { mime: file.type, bits: await fileToBase64(file), width, height, filename: file.name, size: file.size }) : Drafty.insertImage(undefined, 0, { mime: file.type, - refurl: (await uploadFile(file, topic.name)).url, + refurl: upgradeToHttps((await uploadFile(file, topic.name)).url), width, height, filename: file.name, @@ -58,7 +58,7 @@ export async function sendFile(topic: Topic, file: File): Promise { const content = file.size <= MAX_INBAND_ATTACHMENT_SIZE ? Drafty.attachFile(undefined, { mime: file.type, data: await fileToBase64(file), filename: file.name, size: file.size }) - : Drafty.attachFile(undefined, { mime: file.type, refurl: (await uploadFile(file, topic.name)).url, filename: file.name, size: file.size }); + : Drafty.attachFile(undefined, { mime: file.type, refurl: upgradeToHttps((await uploadFile(file, topic.name)).url), filename: file.name, size: file.size }); const msg = topic.createMessage(content, false); msg.head = Object.assign(msg.head || {}, { 'x-im-type': 'file', 'x-im-client-mid': newClientMid() }); return topic.publishDraft(msg, Promise.resolve()); @@ -79,7 +79,7 @@ export async function sendVoice(topic: Topic, blob: Blob, durationMs: number): P ? Drafty.insertAudio(undefined, 0, { mime: file.type, bits: await fileToBase64(file), duration: durationMs, size: file.size, filename: file.name }) : Drafty.insertAudio(undefined, 0, { mime: file.type, - refurl: (await uploadFile(file, topic.name)).url, + refurl: upgradeToHttps((await uploadFile(file, topic.name)).url), duration: durationMs, size: file.size, filename: file.name, diff --git a/web/vite.config.ts b/web/vite.config.ts index 5488b850..1ffc1ba1 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ __APP_VERSION__: JSON.stringify(pkg.version), }, server: { - port: 8094, + port: process.env.PORT ? Number(process.env.PORT) : 8094, // Same-origin dev proxy for the REST layer only -- avoids CORS entirely (the browser only ever // sees http://localhost:8094) so a resolved tenant's *relative* endpoints ("/v1/im", "/v0/file") // work unmodified in local dev, same idea as the old app's hand-rolled dev-proxy-server.js, just