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

Expo React Native上传PDF至DRF路径错误求助

Expo React Native 上传PDF至DRF API路径错误排查

问题概述

开发Expo React Native应用时,将PDF文件上传到Django Rest Framework搭建的API后,文件未保存到指定的media/pdfs/目录,而是直接存入media根目录,导致访问时返回404错误。浏览器端上传一切正常,但Expo应用上传出现路径异常。


后端代码

models.py

# models.py
from django.db import models

class PDFFile(models.Model):
    name = models.CharField(max_length=200, blank=True)
    pdf_file = models.FileField(upload_to="pdfs/", blank=True)

    def __str__(self):
        return str(self.name)

serializers.py

# serializers.py
from rest_framework import serializers
from .models import PDFFile

class PDFFileModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = PDFFile
        fields = ["id", "name", "pdf_file"]

views.py

# views.py
from .models import PDFFile
from .serializers import PDFFileModelSerializer

from rest_framework import viewsets, status
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response

class PDFFileViewSet(viewsets.ModelViewSet):
    queryset = PDFFile.objects.all()
    serializer_class = PDFFileModelSerializer
    parser_classes = (MultiPartParser, FormParser)

    def create(self, request, *args, **kwargs):
        name = request.data["name"]
        pdf_file = request.data["pdf_file"]
        print("PDF FILE: ", pdf_file)

        PDFFile.objects.create(name=name, pdf_file=pdf_file)

        return Response("PDF create with success!", status=status.HTTP_200_OK)

应用urls.py

# urls.py (app)
from rest_framework import routers
from .views import PDFFileViewSet
from django.urls import path, include

router = routers.DefaultRouter()
router.register("pdfs", PDFFileViewSet)

urlpatterns = [
    path("", include(router.urls))
]

项目urls.py

# urls.py (project)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include("pdf_uploader.urls")),
]  + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

settings.py

# settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

REST_FRAMEWORK = {
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser'
     )
 }

DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880

STATIC_URL = '/static/'

MEDIA_URL = '/media/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

STATIC_ROOT = BASE_DIR / 'staticfiles'

Expo React Native 组件代码

import { View, Text, StyleSheet, TouchableOpacity, SafeAreaView, Alert } from 'react-native'
import React, { useState } from 'react'
import * as DocumentPicker from 'expo-document-picker'
import { PDF_BASE_URL } from '../data/api'

const PdfPicker = () => {
  
  const [pdfFile, setPdfFile] = useState(null)
  const [name, setName] = useState("")
  const [uploading, setUploading] = useState(false)
  
  const pickerPdf = async () => {
    let result = await DocumentPicker.getDocumentAsync({
      type: 'application/pdf',
    })
    
    const fileName = result.uri.split('/').pop() // extract the file name from the URI
    const source = { uri: fileName, name: result.name }
    console.log("source", source)
    setPdfFile(source.uri)
    setName(source.name)
  }
  
  const uploadPdfFile = async () => {
    setUploading(true)
    
    const uploadPdf = pdfFile
    const pdfName = name
    const data = new FormData()

    data.append("name", pdfName)
    data.append("pdf_file", uploadPdf)

    const response = await fetch(PDF_BASE_URL + "pdfs/", {
      method: "POST",
      headers: {
        "Content-Type": "multipart/form-data",
      },
      body: data
    })

    console.log("RESULT: ", JSON.stringify(response))

    try {
      await JSON.stringify(response)
    } catch (e) {
      console.log("Catch error:", e)
    }
    setUploading(false)
    Alert.alert(
      "Pdf file uploaded..!!"
    )
    setName("")
    setPdfFile(null)
  }

  return (
    <SafeAreaView style={styles.container} >
      <View>
        <Text style={{color: "white", marginBottom: 30}}>{name}</Text>
      </View>
      <TouchableOpacity style={styles.selectButton} onPress={pickerPdf} >
        <Text style={styles.buttonText}>Pick a Pdf file</Text>
      </TouchableOpacity>
      <View style={styles.pdfContainer}>
        <TouchableOpacity style={styles.uploadButton} onPress={uploadPdfFile}>
          <Text style={styles.buttonText}>Upload Pdf file</Text>
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  )
}

export default PdfPicker

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    backgroundColor: '#000',
    justifyContent: 'center',
  },
  selectButton: {
    borderRadius: 5,
    width: 150,
    height: 50,
    backgroundColor: 'blue',
    alignItems: 'center',
    justifyContent: 'center',
  },
  buttonText: {
    color: 'white',
    fontSize: 18,
    fontWeight: 'bold'
  },
  pdfContainer: {
    marginTop: 30,
    marginBottom: 50,
    alignItems: 'center',
  },
  uploadButton: {
    borderRadius: 5,
    width: 150,
    height: 50,
    backgroundColor: 'green',
    alignItems: 'center',
    justifyContent: 'center',
  }
})

日志与返回结果

Expo 日志

LOG  RESULT:  {"type":"default","status":200,"ok":true,"statusText":"","headers":{"map":{"allow":"GET, POST, HEAD, OPTIONS","content-length":"26","content-type":"application/json","cross-origin-opener-policy":"same-origin","date":"Thu, 06 Apr 2023 14:19:30 GMT","referrer-policy":"same-origin","server":"railway","vary":"Accept, Origin, Cookie","x-content-type-options":"nosniff","x-frame-options":"DENY"}},"url":"{my.server}/api/pdfs/","bodyUsed":false,"_bodyInit":{"_data":{"size":26,"offset":0,"blobId":"837A79CD-3B4F-4D1C-A15D-74BE46D9CD26","type":"application/json","name":"pdfs.json","__collector":{}}},"_bodyBlob":{"_data":{"size":26,"offset":0,"blobId":"837A79CD-3B4F-4D1C-A15D-74BE46D9CD26","type":"application/json","name":"pdfs.json","__collector":{}}}}

浏览器上传返回结果

{
        "id": 5,
        "name": "livro",
        "pdf_file": "{my.server}/media/pdfs/Stephen.King.-.A.Balsa_WF9W8Kc.pdf"
    }

Expo 上传返回结果

{
        "id": 4,
        "name": "CERTIDAO-IANDERSONPIZETTEUENDERSONPABLOIII.pdf",
        "pdf_file": "{my.server}/media/C2699748-A1CF-49F5-A3C7-6B527AD3FF44.pdf"
    },

后端日志

Not Found: /media/C2699748-A1CF-49F5-A3C7-6B527AD3FF44.pdf

问题核心

浏览器上传的文件正常保存到media/pdfs/目录,但Expo上传的文件直接存入media根目录,导致访问时返回404。已查阅官方文档并尝试相关工具,仍未解决。


解决方案

问题根源

  1. Expo端提取文件时错误地将URI的最后一段作为uri值,而非完整的文件本地URI;
  2. 构建FormData时未传入符合要求的文件对象,仅传入了字符串文件名,导致DRF无法正确识别文件类型并应用upload_to规则。

修改后的Expo代码

const pickerPdf = async () => {
  let result = await DocumentPicker.getDocumentAsync({
    type: 'application/pdf',
    copyToCacheDirectory: true // 确保文件可被访问
  })
  
  if (!result.canceled) {
    // 保留完整的文件URI,提取原始文件名
    setPdfFile(result.assets[0].uri)
    setName(result.assets[0].name)
  }
}

const uploadPdfFile = async () => {
  setUploading(true)
  
  const data = new FormData()
  // 正确构建FormData文件项,包含uri、文件名和MIME类型
  data.append("name", name)
  data.append("pdf_file", {
    uri: pdfFile,
    name: name,
    type: 'application/pdf'
  })

  try {
    const response = await fetch(PDF_BASE_URL + "pdfs/", {
      method: "POST",
      // 不要手动设置Content-Type,fetch会自动添加正确的boundary分隔符
      headers: {},
      body: data
    })

    const responseData = await response.json()
    console.log("RESULT: ", responseData)
    Alert.alert("PDF文件上传成功!")
  } catch (e) {
    console.log("上传错误:", e)
    Alert.alert("上传失败,请重试")
  } finally {
    setUploading(false)
    setName("")
    setPdfFile(null)
  }
}

后端代码优化(可选)

建议改用序列化器处理创建逻辑,避免直接操作模型,保证验证和逻辑一致性:

def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    return Response(serializer.data, status=status.HTTP_201_CREATED)

修改后,Expo上传的文件会正确保存到media/pdfs/目录,解决404访问错误。


内容的提问来源于stack exchange,提问作者Pablo III

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 13:37:02