fix content type error on android

This commit is contained in:
Oleksii Bulenok
2026-08-04 19:09:50 +02:00
parent 70be0deeb8
commit 10e1930f74
2 changed files with 120 additions and 1 deletions
+90 -1
View File
@@ -1,8 +1,97 @@
diff --git a/build/winter/runtime.native.d.ts b/build/winter/runtime.native.d.ts
index 85c4227e4e37003dd3769a590e73a69874de5fac..27bd245675d09b906c2474d4da9efd073dd4f72c 100644
index 85c4227e4e37003dd3769a590e73a69874de5fac..e389ca8edd2310df9ce29684991465c275e804c5 100644
--- a/build/winter/runtime.native.d.ts
+++ b/build/winter/runtime.native.d.ts
@@ -1,3 +1,2 @@
import 'react-native/Libraries/Core/InitializeCore';
-import '../../types';
//# sourceMappingURL=runtime.native.d.ts.map
\ No newline at end of file
diff --git a/src/winter/fetch/RequestUtils.ts b/src/winter/fetch/RequestUtils.ts
index a93473fd2beaf2ab0f3880951d292e58e7288d10..c27911bd5cb56ba6051fb8ebf4194cced43bc0fd 100644
--- a/src/winter/fetch/RequestUtils.ts
+++ b/src/winter/fetch/RequestUtils.ts
@@ -50,7 +50,11 @@ function isBlob(obj: any): obj is Blob {
*/
export async function normalizeBodyInitAsync(
body: BodyInit | null | undefined
-): Promise<{ body: Uint8Array | null; overriddenHeaders?: NativeHeadersType }> {
+): Promise<{
+ body: Uint8Array | null;
+ overriddenHeaders?: NativeHeadersType;
+ fallbackHeaders?: NativeHeadersType;
+}> {
if (body == null) {
return { body: null };
}
@@ -71,7 +75,12 @@ export async function normalizeBodyInitAsync(
if (body instanceof Blob || isBlob(body)) {
return {
body: new Uint8Array(await blobToArrayBufferAsync(body)),
- overriddenHeaders: [['Content-Type', body.type]],
+ /*
+ * Per the fetch spec, a blob's type is only a default for Content-Type:
+ * it must not replace a header the caller set explicitly, and an empty
+ * type contributes no header at all.
+ */
+ fallbackHeaders: body.type ? [['Content-Type', body.type]] : undefined,
};
}
@@ -137,6 +146,25 @@ export function overrideHeaders(
return result;
}
+/**
+ * Create a new header array by adding new headers only for keys not already
+ * present (by case-insensitive header key). Used for body-derived defaults
+ * that must not replace caller-provided headers.
+ */
+export function fillMissingHeaders(
+ headers: NativeHeadersType,
+ newHeaders: NativeHeadersType
+): NativeHeadersType {
+ const existingKeySet = new Set(headers.map(([key]) => key.toLocaleLowerCase()));
+ const result: NativeHeadersType = [...headers];
+ for (const [key, value] of newHeaders) {
+ if (!existingKeySet.has(key.toLocaleLowerCase())) {
+ result.push([key, value]);
+ }
+ }
+ return result;
+}
+
/** Normalizes known HTTP methods to uppercase */
export function normalizeMethod(method: string): string {
const normalized = method.toUpperCase();
diff --git a/src/winter/fetch/fetch.ts b/src/winter/fetch/fetch.ts
index ac789c024c96379e239bf44903c47e6a8e540d63..4f953b845c145de02976e8162ba2f63f8bd4dfd5 100644
--- a/src/winter/fetch/fetch.ts
+++ b/src/winter/fetch/fetch.ts
@@ -3,6 +3,7 @@ import { FetchError } from './FetchErrors';
import { FetchResponse, type AbortSubscriptionCleanupFunction } from './FetchResponse';
import type { NativeRequest, NativeRequestInit } from './NativeRequest';
import {
+ fillMissingHeaders,
normalizeBodyInitAsync,
normalizeHeadersInit,
overrideHeaders,
@@ -62,10 +63,17 @@ export async function fetch(
const request = new ExpoFetchModule.NativeRequest(response) as NativeRequest;
- const { body: requestBody, overriddenHeaders } = await normalizeBodyInitAsync(body);
+ const {
+ body: requestBody,
+ overriddenHeaders,
+ fallbackHeaders,
+ } = await normalizeBodyInitAsync(body);
if (overriddenHeaders) {
headers = overrideHeaders(headers, overriddenHeaders);
}
+ if (fallbackHeaders) {
+ headers = fillMissingHeaders(headers, fallbackHeaders);
+ }
const nativeRequestInit: NativeRequestInit = {
credentials: credentials ?? 'include',
+30
View File
@@ -24,3 +24,33 @@ import.
Can be removed if Expo stops referencing `./react-native-web` from the types
loaded by the native winter runtime, or guards the augmentation to web.
## src/winter/fetch/RequestUtils.ts + fetch.ts - Blob body must not clobber an explicit Content-Type
Expo 57 installs `expo/fetch` as the global `fetch` on native
(`src/winter/runtime.native.ts`), replacing React Native's whatwg-fetch. When
the request body is a Blob, `normalizeBodyInitAsync` returned
`overriddenHeaders: [['Content-Type', blob.type]]`, which `fetch.ts` applied
*over* the caller's headers (introduced in expo/expo#33405). This is backwards
per the fetch spec: a blob's type is only a default, used when no Content-Type
was provided, and an empty type must contribute no header at all.
In this app it broke publishing posts with any image blob on Android. The
composer uploads via `agent.uploadBlob(blob, {encoding})`; `@atproto/xrpc`
sets `content-type: <mime>` explicitly, but the blob comes from an XHR
`file://` read of a `.bin`-renamed jpeg (the RN#27099 workaround in
`src/lib/api/upload-blob.ts`), for which Android's BlobModule returns an empty
mime. expo/fetch replaced the good header with the empty blob type and the PDS
rejected the upload with "Request encoding (Content-Type) required but not
provided". Also silently rewrote the intended mime on every other blob upload
(avatars, banners, caption files) even when the blob type was non-empty.
The patch splits body-derived headers into two channels: FormData keeps
`overriddenHeaders` (its boundary header must win), while the Blob branch
returns new `fallbackHeaders` (skipped entirely when `blob.type` is empty)
that `fetch.ts` applies via `fillMissingHeaders` only for header keys the
caller did not set. This matches browser behavior.
Upstream: expo/expo#33405 introduced the override; the SDK 58 Request rewrite
(expo/expo#46630) is expected to make this spec-compliant, so re-evaluate on
the next SDK bump. Worth filing an issue against expo/expo referencing this.