超大文件上传限制
This commit is contained in:
1 parent
885534378b
commit
3759422222
9 files changed
+249
-82
No files matched your search
@@ -0,0 +1,72 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { uploadFormDataWithProgress } from './upload-progress'
|
||||
|
||||
class FakeXhr {
|
||||
static current: FakeXhr
|
||||
|
||||
upload = {
|
||||
onprogress: null as ((event: ProgressEvent) => void) | null
|
||||
}
|
||||
|
||||
status = 200
|
||||
statusText = 'OK'
|
||||
responseText = '{"ok":true}'
|
||||
withCredentials = false
|
||||
onload: (() => void) | null = null
|
||||
onerror: (() => void) | null = null
|
||||
|
||||
constructor() {
|
||||
FakeXhr.current = this
|
||||
}
|
||||
|
||||
open() {}
|
||||
|
||||
send() {}
|
||||
}
|
||||
|
||||
describe('uploadFormDataWithProgress', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('reports the uploaded byte percentage', async () => {
|
||||
vi.stubGlobal('XMLHttpRequest', FakeXhr)
|
||||
const onProgress = vi.fn()
|
||||
const result = uploadFormDataWithProgress<{ ok: boolean }>(
|
||||
'/upload',
|
||||
new FormData(),
|
||||
onProgress
|
||||
)
|
||||
|
||||
FakeXhr.current.upload.onprogress!({
|
||||
lengthComputable: true,
|
||||
loaded: 40,
|
||||
total: 100
|
||||
} as ProgressEvent)
|
||||
FakeXhr.current.onload!()
|
||||
|
||||
await expect(result).resolves.toEqual({ ok: true })
|
||||
expect(onProgress).toHaveBeenCalledWith(40)
|
||||
})
|
||||
|
||||
it('keeps the HTTP status and server error message', async () => {
|
||||
vi.stubGlobal('XMLHttpRequest', FakeXhr)
|
||||
const result = uploadFormDataWithProgress(
|
||||
'/upload',
|
||||
new FormData(),
|
||||
() => {}
|
||||
)
|
||||
|
||||
FakeXhr.current.status = 401
|
||||
FakeXhr.current.statusText = 'Unauthorized'
|
||||
FakeXhr.current.responseText = JSON.stringify({
|
||||
data: { error: { message: '未登录' } }
|
||||
})
|
||||
FakeXhr.current.onload!()
|
||||
|
||||
await expect(result).rejects.toMatchObject({
|
||||
statusCode: 401,
|
||||
message: '未登录'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
interface UploadError extends Error {
|
||||
statusCode: number
|
||||
statusMessage: string
|
||||
data: unknown
|
||||
}
|
||||
|
||||
function uploadErrorMessage(data: unknown, fallback: string): string {
|
||||
if (!data || typeof data !== 'object') return fallback
|
||||
|
||||
const body = data as {
|
||||
message?: unknown
|
||||
error?: { message?: unknown }
|
||||
data?: { error?: { message?: unknown } }
|
||||
}
|
||||
const message = body.data?.error?.message ?? body.error?.message ?? body.message
|
||||
return typeof message === 'string' ? message : fallback
|
||||
}
|
||||
|
||||
export function uploadFormDataWithProgress<T>(
|
||||
url: string,
|
||||
formData: FormData,
|
||||
onProgress: (percent: number) => void
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', url)
|
||||
xhr.withCredentials = true
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable && event.total > 0) {
|
||||
onProgress(Math.round((event.loaded / event.total) * 100))
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onload = () => {
|
||||
let data: unknown = null
|
||||
try {
|
||||
data = xhr.responseText ? JSON.parse(xhr.responseText) : null
|
||||
} catch {
|
||||
// 非 JSON 响应交给状态码和状态文本报错
|
||||
}
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(data as T)
|
||||
return
|
||||
}
|
||||
|
||||
const error = new Error(
|
||||
uploadErrorMessage(data, xhr.statusText || 'Upload failed')
|
||||
) as UploadError
|
||||
error.statusCode = xhr.status
|
||||
error.statusMessage = xhr.statusText
|
||||
error.data = data
|
||||
reject(error)
|
||||
}
|
||||
|
||||
xhr.onerror = () => reject(new Error('Network error'))
|
||||
xhr.send(formData)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user