Hosted uploads larger than the 4 MB multipart ceiling (Vercel's 4.5 MB request-body cap) now go POST /upload/create (signed PUT URL, rate-limited) -> PUT to the raw Storage URL -> POST /upload/complete (server-side magic-byte and size validation, sha256, WORM move, idempotent), reusing the #1378 pending-upload primitives. uploadAndExtract is split into uploadDocument + processArchivedDocument so both paths share the inbox pipeline. Dokumentinkorgen and the supplier-invoice form use the new path only above the threshold; files that fit keep the multipart route. Cap stays at 10 MB (the issue asks for 20 MB: founder call). Refs #1551
This commit is contained in:
@@ -77,8 +77,13 @@ import {
|
||||
getResponseErrorMessage,
|
||||
} from '@/lib/errors/get-error-message'
|
||||
import { notifySessionExpired } from '@/lib/auth/session-timeout-shared'
|
||||
import { exceedsHostedUploadLimit, tooLargeMessage } from '@/lib/documents/upload-size'
|
||||
import {
|
||||
exceedsHostedUploadLimit,
|
||||
exceedsInboxUploadLimit,
|
||||
inboxTooLargeMessage,
|
||||
} from '@/lib/documents/upload-size'
|
||||
import { shrinkImageForUpload } from '@/lib/documents/shrink-image'
|
||||
import { uploadViaSignedUrl } from '@/lib/documents/direct-upload'
|
||||
|
||||
type AccountingMethod = 'accrual' | 'cash'
|
||||
|
||||
@@ -879,25 +884,28 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
) => {
|
||||
// A phone photo is routinely larger than the request body the platform
|
||||
// will carry, and it rejects the upload itself, before the route can say
|
||||
// anything useful about it. Shrink what can be shrunk, and refuse the rest
|
||||
// here, where we can name the size instead of letting the transfer fail.
|
||||
// anything useful about it. Shrink what can be shrunk; what cannot be (a
|
||||
// scanned PDF) goes straight to Storage through a signed URL instead of a
|
||||
// multipart body. Only the inbox's own ceiling refuses anything now, here,
|
||||
// where we can name the size instead of letting the transfer fail.
|
||||
const file = exceedsHostedUploadLimit(original.size)
|
||||
? await shrinkImageForUpload(original)
|
||||
: original
|
||||
if (exceedsHostedUploadLimit(file.size)) {
|
||||
if (exceedsInboxUploadLimit(file.size)) {
|
||||
reportUploadFailure({
|
||||
status: 0,
|
||||
size: file.size,
|
||||
type: file.type || 'unknown',
|
||||
reason: 'over hosted body limit, refused client-side',
|
||||
reason: 'over inbox ceiling, refused client-side',
|
||||
})
|
||||
toast({
|
||||
title: 'Uppladdning misslyckades',
|
||||
description: tooLargeMessage(file.size),
|
||||
description: inboxTooLargeMessage(file.size),
|
||||
variant: 'destructive',
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
const directToStorage = exceedsHostedUploadLimit(file.size)
|
||||
|
||||
// Optimistic placeholder: gives the user an immediate visual response
|
||||
// for the 3-8s while extraction runs. Removed once the real row arrives.
|
||||
@@ -929,12 +937,17 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
}
|
||||
setIsUploading(true)
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
const res = await fetch('/api/extensions/ext/invoice-inbox/upload', {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
})
|
||||
let res: Response
|
||||
if (directToStorage) {
|
||||
res = await uploadViaSignedUrl(file)
|
||||
} else {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
res = await fetch('/api/extensions/ext/invoice-inbox/upload', {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
})
|
||||
}
|
||||
if (!res.ok) throw await resolveFailure(res)
|
||||
const json = await res.json()
|
||||
if (json.data?.extraction_skipped) {
|
||||
|
||||
@@ -29,6 +29,8 @@ import { getAccountDescription } from '@/lib/bookkeeping/account-descriptions'
|
||||
import { useBasReference } from '@/lib/bookkeeping/use-bas-reference'
|
||||
import { formatCounterpartyName } from '@/lib/bookkeeping/counterparty-templates'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { exceedsHostedUploadLimit } from '@/lib/documents/upload-size'
|
||||
import { uploadViaSignedUrl } from '@/lib/documents/direct-upload'
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { getDisplayTotal } from '@/lib/invoices/rounding'
|
||||
import { useUnsavedChanges } from '@/lib/hooks/use-unsaved-changes'
|
||||
@@ -1248,13 +1250,23 @@ export default function NewSupplierInvoiceForm({
|
||||
setExtractionPhase('idle')
|
||||
setPendingExtraction(null)
|
||||
|
||||
// Above the hosted request-body limit the platform refuses a multipart
|
||||
// body before the route runs (a plain 413, nothing in the logs), so the
|
||||
// bytes go straight to Storage through a signed URL instead. Same
|
||||
// response shape either way.
|
||||
const directToStorage = exceedsHostedUploadLimit(file.size)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await fetch('/api/extensions/ext/invoice-inbox/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
let res: Response
|
||||
if (directToStorage) {
|
||||
res = await uploadViaSignedUrl(file)
|
||||
} else {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
res = await fetch('/api/extensions/ext/invoice-inbox/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
if (res.ok) {
|
||||
const json = await res.json().catch(() => null)
|
||||
const data = json?.data as
|
||||
@@ -1286,6 +1298,12 @@ export default function NewSupplierInvoiceForm({
|
||||
} catch {
|
||||
// Extension unreachable: fall through to the plain upload below.
|
||||
}
|
||||
if (directToStorage) {
|
||||
// The plain fallback posts a multipart body to /api/documents, which
|
||||
// the platform would refuse the same way: nothing left to try.
|
||||
setDocumentFiles([{ ...entry, status: 'error', error: t('underlag_upload_failed') }])
|
||||
return
|
||||
}
|
||||
await uploadPlainDocument(entry)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user