You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo+AWS SDK上传图片至DigitalOcean Spaces遇网络请求失败

问题:Expo + AWS SDK 上传图片到DigitalOcean Spaces 报Network request failed错误

错误信息

ERROR  Error uploading image: [TypeError: Network request failed]

相关代码

import AsyncStorage from '@react-native-async-storage/async-storage';
import axios from 'axios';
import * as ImagePicker from 'expo-image-picker';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import 'react-native-get-random-values'; // This polyfills crypto.getRandomValues
import { v4 as uuidv4 } from 'uuid';
import { spaces } from '../config/digitalOcean';
import 'react-native-url-polyfill/auto';
import { ReadableStream } from 'web-streams-polyfill';
globalThis.ReadableStream = ReadableStream;

export default function AddItems({ navigation }) {
  const [produceName, setProduceName] = useState('');
  const [quantity, setQuantity] = useState('');
  const [price, setPrice] = useState('');
  const [itemImage, setItemImage] = useState('');
  const [phoneNumber, setPhoneNumber] = useState('');
  const [uploading, setUploading] = useState(false);

  const s3Client = new S3Client({
    endpoint: spaces.url,
    forcePathStyle: false,
    region: "blr1",
    credentials: {
      accessKeyId: spaces.spacesKey,
      secretAccessKey: spaces.spacesSecret
    }
  });

  useEffect(() => {
    const loadPhoneNumber = async () => {
      try {
        const storedPhoneNumber = await AsyncStorage.getItem('phone');
        if (storedPhoneNumber) {
          setPhoneNumber(storedPhoneNumber);
        } else {
          Alert.alert('Error', 'Phone number not found');
        }
      } catch (error) {
        console.error('Error loading phone number from AsyncStorage:', error);
      }
    };

    loadPhoneNumber();
  }, []);

  const pickImageAsync = async () => {
    let result = await ImagePicker.launchImageLibraryAsync({
      allowsEditing: true,
      quality: 1,
    });

    if (!result.canceled) {
      uploadImage(result.assets[0]);
    } else {
      alert('You did not select any image.');
    }
  };

  const getImageBlob = (uri) => {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.onload = function() {
        resolve(xhr.response);
      };
      xhr.onerror = function() {
        reject(new Error('XMLHttpRequest failed'));
      };
      xhr.responseType = 'blob';
      xhr.open('GET', uri, true);
      xhr.send(null);
    });
  };

  const uploadImage = async (imageAsset) => {
    setUploading(true);
    const fileName = `${uuidv4()}.jpg`;

    try {
      const response = await fetch(imageAsset.uri);
      const blob = await getImageBlob(imageAsset.uri);
      console.log("these happened without error");
      console.log(response);
      console.log(blob);

      const params = {
        Bucket: spaces.name,
        Key: fileName,
        Body: blob,
        ACL: "public-read",
        ContentType: 'multipart/form-data',
      };

      const data = await s3Client.send(new PutObjectCommand(params));
      console.log(data);

      const imageUrl = spaces.url + "/" + fileName;
      setItemImage(imageUrl);
      Alert.alert('Success', 'Image uploaded successfully');
    } catch (error) {
      console.error('Error uploading image:', error);
      Alert.alert('Error', 'Failed to upload image');
    }
    setUploading(false);
  };

  const handleSubmit = async () => {
    if (!produceName || !quantity || !price) {
      Alert.alert('Error', 'All fields are required');
      return;
    }

    try {
      const formData = {
        phoneNumber,
        itemName: produceName,
        quantity,
        price,
        itemImage,
      };

      const response = await axios.post(
        'myurl/api/supplier/add-item',
        formData
      );

      if (response.status === 200) {
        Alert.alert('Success', 'Item added successfully!');
        navigation.goBack();
      }
    } catch (error) {
      console.error('Error adding item:', error.response?.data || error.message);
      Alert.alert(
        'Error',
        error.response?.data?.message || 'Failed to add item, please try again'
      );
    }
  };
}

已尝试的操作

  • 确认DigitalOcean Spaces的凭证有效。
  • 反复核对spaces.url、accessKeyId和secretAccessKey的正确性。

问题分析与解决步骤

1. Content-Type 配置错误

你在PutObjectCommand的参数里设置了ContentType: 'multipart/form-data',这是错误的。上传单张图片时,应该使用图片实际的MIME类型(比如image/jpeg),可以直接从imageAsset对象中获取正确类型:

const params = {
  Bucket: spaces.name,
  Key: fileName,
  Body: blob,
  ACL: "public-read",
  ContentType: imageAsset.mimeType, // 替换为图片实际的MIME类型
};

2. Blob处理冗余且存在兼容性风险

你同时使用了fetch(imageAsset.uri)和自定义的getImageBlob方法,完全可以简化为直接通过fetch获取blob,避免XMLHttpRequest在React Native环境下的潜在问题:

// 替换原有的response和blob获取代码
const blob = await fetch(imageAsset.uri).then(res => res.blob());

3. 端点与路径样式配置验证

  • 确保spaces.url格式正确,blr1区域的端点应为https://blr1.digitaloceanspaces.com,必须包含https://协议,且末尾不要加斜杠。
  • 尝试将forcePathStyle设置为true再测试,部分S3兼容存储需要开启路径样式访问:
    const s3Client = new S3Client({
      endpoint: spaces.url,
      forcePathStyle: true, // 修改为true
      region: "blr1",
      credentials: {
        accessKeyId: spaces.spacesKey,
        secretAccessKey: spaces.spacesSecret
      }
    });
    

4. 网络权限与CORS配置检查

  • 确认Expo项目的网络权限:iOS需在app.json中配置NSAppTransportSecurity允许HTTPS请求;Android需在AndroidManifest.xml中添加<uses-permission android:name="android.permission.INTERNET" />。
  • 检查DigitalOcean Spaces的CORS规则,确保允许你的应用发起PUT请求,开发环境需允许localhost相关域名。

5. AWS SDK 兼容性验证

AWS SDK v3在React Native环境下需要完整的polyfill支持,确认已安装并正确引入@aws-sdk/util-stream-node,同时确保ReadableStream的polyfill全局生效。

内容的提问来源于stack exchange,提问作者Samarth Bagga

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 12:53:17