- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 140
 
Description
Is this a Bug?
- I have confirmed that I want to report a Bug
 
Has this issue been reported before?
- I have confirmed that this Issue has not been reported before
 
Alova Version
^3.3.1
Framework
React
Problem Description
react native expo, post上传文件是自动拼接Content-type
Expected Behavior
/**
- 修复版的 fetch adapter,解决 React Native 中 FormData 检测失败的问题
 - 修复:alova/fetch 使用 data.toString() 检测 FormData,在 RN 中会失败
*/
import { ObjectCls, JSONStringify, setTimeoutFn, clearTimeoutTimer, promiseReject, newInstance, undefinedValue, isString, isSpecialRequestBody, falseValue, trueValue } from '@alova/shared'; 
const isBodyData = (data: any) => isString(data) || isSpecialRequestBody(data);
const isFormDataFixed = (data: any): boolean => {
if (!data) return false;
const type = Object.prototype.toString.call(data);
return type === '[object FormData]' ||
(type === '[object Object]' && '_parts' in data && Array.isArray(data._parts)) ||
data instanceof FormData;
};
function adapterFetchFixed() {
return (elements: any, method: any) => {
const adapterConfig = method.config;
const timeout = adapterConfig.timeout || 0;
const ctrl = new AbortController();
const { data, headers } = elements;
const isContentTypeSet = /content-type/i.test(ObjectCls.keys(headers).join());
const isDataFormData = isFormDataFixed(data);
if (!isContentTypeSet && !isDataFormData) {
  headers['Content-Type'] = 'application/json;charset=UTF-8';
}
const body = isDataFormData ? data : (isBodyData(data) ? data : JSONStringify(data));
let abortTimer: any;
let isTimeout = falseValue;
if (timeout > 0) {
  abortTimer = setTimeoutFn(() => {
    isTimeout = trueValue;
    ctrl.abort();
  }, timeout);
}
const fetchPromise = fetch(elements.url, {
  ...adapterConfig,
  method: elements.type,
  signal: ctrl.signal,
  body
});
return {
  response: () => fetchPromise.then(response => {
    // Clear interrupt processing after successful request
    clearTimeoutTimer(abortTimer);
    // Response's readable can only be read once and needs to be cloned before it can be reused.
    return response.clone();
  }, err => promiseReject(isTimeout ? newInstance(Error, 'fetchError: network timeout') : err)),
  // The then in the Headers function needs to catch exceptions, otherwise the correct error object will not be obtained internally.
  headers: () => fetchPromise.then(({ headers: responseHeaders }: Response) => responseHeaders, () => ({})),
  // Due to limitations of the node fetch library, this code cannot be unit tested, but it has passed the test in the browser.
  /* c8 ignore start */
  onDownload: async (cb: (loaded: number, total: number) => void) => {
    let isAborted = falseValue;
    const response = await fetchPromise.catch(() => {
      isAborted = trueValue;
    });
    if (!response)
      return;
    const { headers: responseHeaders, body } = response.clone();
    const reader = body ? body.getReader() : undefinedValue;
    const total = Number(responseHeaders.get('Content-Length') || responseHeaders.get('content-length') || 0);
    if (total <= 0) {
      return;
    }
    let loaded = 0;
    if (reader) {
      const pump = () => reader.read().then(({ done, value = new Uint8Array() }: ReadableStreamReadResult<Uint8Array>) => {
        if (done || isAborted) {
          isAborted && cb(loaded, 0);
        }
        else {
          loaded += value.byteLength;
          cb(loaded, total);
          return pump();
        }
      });
      pump();
    }
  },
  onUpload() {},
  /* c8 ignore stop */
  abort: () => {
    ctrl.abort();
    clearTimeoutTimer(abortTimer);
  }
};
};
}
export { adapterFetchFixed as default };
Reproduction Link
No response
Reproduction Steps
import { createAlova } from "alova";
import adapterFetch from "alova/fetch";
import ReactHook from "alova/react";
import { Page } from "./page";
import { Result } from "./result";
import { Platform } from "react-native";
// 调试开关 - 可以通过环境变量控制
const DEBUG_API = DEV && true; // 开发环境下默认开启
export const API_BASE_URL = "http://127.0.0.1:8080";
export const AlovaInstance = createAlova({
baseURL: API_BASE_URL,
statesHook: ReactHook,
requestAdapter: adapterFetch(),
shareRequest: true,
timeout: 10000,
beforeRequest(req) {
req.config.headers = {
v: "1.0.3",
t: Date.now().toString(),
d: Platform.OS.toUpperCase(),
};
// 调试信息打印
if (DEBUG_API) {
  const url = req.url.startsWith("http")
    ? new URL(req.url)
    : { pathname: req.url, search: "" };
  const queryParams = url.search ? url.search.substring(1) : "";
  const body = req.data ? JSON.stringify(req.data) : "";
  const method = (req.type || "GET").toUpperCase();
  console.log(
    `request method: ${method} url: ${url.pathname}${queryParams ? `, query params: ${queryParams}` : ""}${body ? `, body: ${body}` : ""}`,
  );
}
},
responded: {
onSuccess: async (res: Response) => {
let result: Result;
try {
result = await res.json();
} catch (e) {
// JSON 解析失败
if (DEBUG_API) {
console.log(response parse error: ${e});
}
throw e;
}
  // 调试信息打印
  if (DEBUG_API) {
    console.log(`response body: ${JSON.stringify(result)}`);
  }
  if (!res.ok) {
    // HTTP 状态码不是 2xx
    if (DEBUG_API) {
      console.log(`response status error: ${res.status} ${res.statusText}`);
    }
  }
  return res.ok ? result : Promise.reject(result);
},
onError(error) {
  // 调试信息打印
  if (DEBUG_API) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    const errorObj = error && typeof error === "object" && "msg" in error ? error : { error };
    console.log(`request error: ${errorMsg}`, errorObj);
  }
  return Promise.reject(error);
},
},
});
const requestConfig = { credentials: "include" } satisfies Partial;
export const Alova = {
upload(url: string, formData: FormData): Promise<Result> {
return AlovaInstance.Post<Result>(url, {
...requestConfig,
data: formData,
});
},
};
System Information
System:
    OS: macOS 15.5
    CPU: (8) arm64 Apple M1
    Memory: 86.77 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.14.0 - /opt/homebrew/bin/node
    Yarn: 4.10.3 - /opt/homebrew/bin/yarn
    npm: 10.9.2 - /opt/homebrew/bin/npm
    Watchman: 2025.04.14.00 - /opt/homebrew/bin/watchman
  Browsers:
    Chrome: 141.0.7390.123
    Safari: 18.5
"alova": "^3.3.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",Additional Information
No response