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
相关产品推荐
相关产品推荐

