Fazer upload de arquivos com Web Workers e fluxos limitados
Use um worker quando seu pipeline de upload precisar processar arquivos fora da thread principal
da página.
Este exemplo lê um File incrementalmente, envia um bloco de 1 MiB por vez por worker e aguarda
a confirmação final do receptor antes de mostrar um link de download. Dois workers podem lidar
com arquivos diferentes enquanto os demais aguardam em uma fila limitada.
Desafios dos uploads de arquivos tradicionais
Um upload assíncrono comum pode adicionar um File diretamente a FormData. Não é necessário
lê-lo primeiro com FileReader; veja os exemplos de FormData da MDN.
Comece por aí se você só precisa enviar um arquivo. Workers não aumentam a largura de banda da
rede, e este tutorial não faz afirmações sobre velocidade ou memória total.
A tarefa específica aqui é controlar leituras incrementais e confirmações entre vários arquivos. A introdução aos workers aborda mensagens básicas de workers. O tutorial de blocos paralelos aborda requisições simultâneas para um mesmo arquivo; aqui, as requisições de cada arquivo são sequenciais.
Conheça os Web Workers e os fluxos
Web Workers
Workers podem processar arquivos e fazer requisições, mas não podem atualizar o DOM da página.
A página envia cada File por clonagem estruturada e renderiza as mensagens do seu worker. Ela não
lê o arquivo em um ArrayBuffer para enviá-lo. Veja como usar Web Workers.
Fluxos JavaScript
Blob.stream(), herdado por File,
fornece um fluxo de bytes e está disponível dentro de workers. Seus limites de leitura não são os
limites do nosso protocolo de upload. O gerador abaixo combina essas leituras em blocos de 1 MiB,
mantendo intacto um bloco final menor.
Visão geral da arquitetura
A página aceita até quatro arquivos, cada um com no máximo 8 MiB, incluindo arquivos vazios. O pool executa no máximo duas tarefas. Cada worker cria um upload, lê um bloco, aguarda a confirmação do seu POST multipart e, por fim, solicita ao receptor que conclua o upload. O progresso contabiliza os bytes do arquivo já confirmados, não os bytes ainda em trânsito.
O receptor está incluído abaixo. Ele verifica os deslocamentos e os tamanhos, mantém até quatro arquivos na memória e disponibiliza os arquivos concluídos para download. Os uploads expiram 60 segundos após a criação, incluindo os uploads concluídos. Parar o servidor causa a perda de todos os arquivos. Esta é uma demonstração de protocolo local, sem novas tentativas, recuperação após recarregar a página ou armazenamento durável.
Configure a demonstração local de upload
Use Node.js 26.8.1 neste exemplo local e um navegador com suporte a workers de módulo, Blob.stream()
e AbortSignal.timeout(). O fluxo completo foi testado no Linux com Chromium 145. Em 1 de outubro de 2026,
o Node 26 é uma versão Current com manutenção ativa; consulte o cronograma de versões
ao escolher um ambiente de execução para implantação. Este receptor foi projetado para localhost.
No Bash, crie um diretório novo. Se a criação ou a navegação falhar, pare e escolha outro nome antes de salvar os arquivos. Não é necessário instalar dependências nem usar a configuração de build de um projeto que contenha o exemplo.
mkdir worker-stream-demo && cd worker-stream-demo
Salve o HTML como index.html, coloque os dois blocos da thread principal em main.ts na ordem mostrada e
salve o worker e o receptor como upload-worker.ts e server.mts. O receptor remove as anotações
TypeScript ao servir os scripts do navegador; os navegadores nunca executam os arquivos-fonte
.ts diretamente.
Thread principal (main.js)
Salve este código como index.html. Os scripts serão servidos a partir da mesma origem local.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Worker stream uploads</title>
</head>
<body>
<main>
<h1>Worker stream uploads</h1>
<label for="file-input">Up to four files, each at most 8 MiB</label>
<input id="file-input" type="file" multiple />
<button id="upload-files" type="button">Upload files</button>
<button id="cancel-uploads" type="button" disabled>Cancel uploads</button>
<p id="status" role="status">Choose files.</p>
<ul id="results" aria-label="Upload results"></ul>
</main>
<script type="module" src="/main.js"></script>
</body>
</html>
Comece main.ts com este pool. Ele desvincula uma tarefa concluída antes de chamar a interface e
avança a fila mesmo se um callback lançar uma exceção. Uma falha no script do worker ou na
serialização de mensagens cancela todo o pool, em vez de devolver um worker defeituoso à operação.
export type WorkerReply =
| { type: 'progress'; progress: number; message: string }
| { type: 'complete'; message: string; url: string }
| { type: 'error'; message: string }
type Callbacks = {
onProgress?: (progress: number, message: string) => void
onComplete?: (message: string, url: string) => void
onError?: (message: string) => void
}
type Task = { id: string; file: File }
class WorkerPool {
closed = false
workers: Worker[] = []
idleWorkers: Worker[] = []
taskQueue: Task[] = []
taskCallbacks = new Map<string, Callbacks>()
currentTasks = new Map<Worker, Task>()
constructor(script: string, size = 2) {
if (!Number.isInteger(size) || size < 1 || size > 2) throw new Error('Use one or two workers')
try {
for (let index = 0; index < size; index++) {
const worker = new Worker(script, { type: 'module' })
worker.onmessage = (event: MessageEvent<WorkerReply>) => this.handleWorkerMessage(worker, event.data)
worker.onerror = (event) => {
event.preventDefault()
this.terminate('An upload worker failed. Try again.')
}
worker.onmessageerror = () => this.terminate('An upload worker failed. Try again.')
this.workers.push(worker)
this.idleWorkers.push(worker)
}
} catch (error) {
this.terminate()
throw error
}
}
processFile(file: File, callbacks: Callbacks, id = crypto.randomUUID()): string {
if (this.closed) throw new Error('The upload pool is closed.')
if (this.taskCallbacks.size >= 4) throw new Error('The upload queue is full.')
const task = { id, file }
this.taskCallbacks.set(task.id, callbacks)
const worker = this.idleWorkers.pop()
if (worker) this.runTask(worker, task)
else this.taskQueue.push(task)
return task.id
}
runTask(worker: Worker, task: Task): void {
this.currentTasks.set(worker, task)
try {
worker.postMessage(task)
} catch {
this.terminate('An upload worker failed. Try again.')
}
}
handleWorkerMessage(worker: Worker, data: WorkerReply): void {
if (this.closed) return
const task = this.currentTasks.get(worker)
if (!task) return
const callbacks = this.taskCallbacks.get(task.id)
if (!callbacks) return
if (data.type === 'progress') {
callbacks.onProgress?.(data.progress, data.message)
return
}
this.currentTasks.delete(worker)
this.taskCallbacks.delete(task.id)
try {
if (data.type === 'complete') callbacks.onComplete?.(data.message, data.url)
else callbacks.onError?.(data.message)
} finally {
if (!this.closed) {
const next = this.taskQueue.shift()
if (next) this.runTask(worker, next)
else this.idleWorkers.push(worker)
}
}
}
terminate(message = 'Upload canceled.'): void {
if (this.closed) return
this.closed = true
for (const worker of this.workers) worker.terminate()
this.workers = []
this.idleWorkers = []
this.taskQueue = []
this.currentTasks.clear()
const callbacks = [...this.taskCallbacks.values()]
this.taskCallbacks.clear()
const errors: unknown[] = []
for (const callback of callbacks) {
try {
callback.onError?.(message)
} catch (error) {
errors.push(error)
}
}
if (errors.length > 0) throw new AggregateError(errors, 'Upload cancellation callbacks failed.')
}
}
Acrescente este bloco a main.ts. A seleção de arquivos e os envios repetidos ficam desabilitados
enquanto um lote está em execução ou em limpeza. Cada linha pertence a um arquivo, portanto uma
falha não pode ser confundida com o resultado de outro arquivo. Os nomes são inseridos como texto,
nunca interpretados como HTML ou caminhos de armazenamento.
const input = document.getElementById('file-input')
const upload = document.getElementById('upload-files')
const cancel = document.getElementById('cancel-uploads')
const status = document.getElementById('status')
const results = document.getElementById('results')
if (!(input instanceof HTMLInputElement) || !(upload instanceof HTMLButtonElement) ||
!(cancel instanceof HTMLButtonElement) || !status || !results) throw new Error('Missing controls')
let pool: WorkerPool | null = null
let running = false
cancel.addEventListener('click', () => pool?.terminate())
window.addEventListener('pagehide', () => pool?.terminate())
input.addEventListener('change', () => {
if (!running) status.textContent = 'Ready to upload.'
})
upload.addEventListener('click', async () => {
if (running) return
const files = Array.from(input.files ?? [])
if (files.length < 1 || files.length > 4 || files.some((file) => file.size > 8 * 1024 * 1024)) {
status.textContent = 'Choose one to four files, each at most 8 MiB.'
return
}
running = true
input.disabled = upload.disabled = true
cancel.disabled = false
results.replaceChildren()
status.textContent = 'Uploading…'
const failedIds: string[] = []
let failures = 0
try {
const batchPool = new WorkerPool('/upload-worker.js')
pool = batchPool
const jobs = files.map((file) => new Promise<void>((resolve) => {
const id = crypto.randomUUID()
const row = document.createElement('li')
const name = document.createElement('p')
name.textContent = file.name
const progress = document.createElement('progress')
progress.max = 100
progress.value = 0
progress.setAttribute('aria-label', `Acknowledged bytes for ${file.name}`)
const message = document.createElement('p')
message.setAttribute('role', 'status')
message.textContent = 'Queued.'
row.append(name, progress, message)
results.append(row)
batchPool.processFile(file, {
onProgress(value, text) { progress.value = value; message.textContent = text },
onComplete(text, url) {
progress.value = 100
message.textContent = text
const link = document.createElement('a')
link.href = url
link.download = 'upload.bin'
link.textContent = `Download ${file.name}`
row.append(link)
resolve()
},
onError(text) {
failures++
failedIds.push(id)
message.textContent = text
resolve()
},
}, id)
}))
await Promise.all(jobs)
status.textContent = failures === 0 ? 'All uploads complete.' : 'Some uploads did not complete.'
} catch {
status.textContent = 'Could not start the upload workers. Try again.'
} finally {
pool?.terminate()
pool = null
let cleanupFailed = false
for (const id of failedIds) {
try {
const response = await fetch(`/uploads/${id}`, {
method: 'DELETE', headers: { 'X-Upload-Demo': '1' }, signal: AbortSignal.timeout(5000),
})
if (!response.ok) cleanupFailed = true
} catch { cleanupFailed = true }
}
if (cleanupFailed) status.textContent += ' Cleanup was not confirmed; uploads will expire.'
running = false
input.disabled = upload.disabled = false
cancel.disabled = true
}
})
Implementação do worker (upload-worker.js)
Salve este código como upload-worker.ts. Aguardar cada POST impede que o gerador produza outro bloco da
aplicação enquanto a requisição anterior estiver pendente. O navegador ainda pode armazenar
internamente em buffers as leituras do fluxo e os dados das requisições. O tamanho de bloco de
1 MiB não é um limite para a memória total do navegador.
import type { WorkerReply } from './main.js'
declare const self: DedicatedWorkerGlobalScope
const CHUNK_SIZE = 1024 * 1024
async function* readChunks(file: Blob): AsyncGenerator<Uint8Array<ArrayBuffer>> {
const reader = file.stream().getReader()
let buffer = new Uint8Array(CHUNK_SIZE)
let used = 0
let finished = false
try {
while (true) {
const { done, value } = await reader.read()
if (done) { finished = true; break }
let offset = 0
while (offset < value.length) {
const length = Math.min(CHUNK_SIZE - used, value.length - offset)
buffer.set(value.subarray(offset, offset + length), used)
used += length
offset += length
if (used === CHUNK_SIZE) {
yield buffer
buffer = new Uint8Array(CHUNK_SIZE)
used = 0
}
}
}
if (used > 0) yield buffer.subarray(0, used)
} finally {
if (!finished) await reader.cancel().catch(() => {})
reader.releaseLock()
}
}
function send(message: WorkerReply): void { self.postMessage(message) }
async function request(path: string, options: RequestInit): Promise<Response> {
const response = await fetch(path, {
...options, headers: { ...options.headers, 'X-Upload-Demo': '1' },
signal: AbortSignal.timeout(5000),
})
if (!response.ok) throw new Error('Upload request rejected')
return response
}
self.onmessage = async (event: MessageEvent<{ file: File; id: string }>) => {
const { file, id } = event.data
try {
if (!(file instanceof File) || file.size > 8 * CHUNK_SIZE) throw new Error('Invalid file')
await request(`/uploads/${id}`, { method: 'POST', headers: { 'Upload-Length': String(file.size) } })
send({ type: 'progress', progress: 0, message: 'Uploading…' })
let acknowledged = 0
for await (const chunk of readChunks(file)) {
const form = new FormData()
form.append('offset', String(acknowledged))
form.append('chunk', new Blob([chunk]), 'chunk.bin')
await request(`/uploads/${id}/chunks`, { method: 'POST', body: form })
acknowledged += chunk.length
send({ type: 'progress', progress: 100 * acknowledged / file.size,
message: `${acknowledged} / ${file.size} bytes acknowledged.` })
}
send({ type: 'progress', progress: 100, message: 'Confirming…' })
const response = await request(`/uploads/${id}/complete`, { method: 'POST' })
if (await response.text() !== String(file.size)) throw new Error('Unexpected acknowledgment')
send({ type: 'complete', message: 'Upload complete.', url: `/uploads/${id}/file` })
} catch {
send({ type: 'error', message: 'Upload failed. Try again.' })
}
}
Este código usa requisições multipart distintas, não um corpo de requisição ReadableStream. Deixe o
navegador definir o delimitador multipart de Content-Type. Um arquivo vazio não envia blocos, mas
ainda exige a conclusão do upload.
Por que não ler o arquivo inteiro de uma vez no worker?
Mover uma chamada arrayBuffer() que lê o arquivo inteiro para um worker ainda cria um buffer da
aplicação com todo o arquivo. Aqui, o agregador mantém um buffer de 1 MiB mais a leitura atual do
fluxo, e cada tarefa ativa tem apenas uma requisição de bloco aguardando confirmação. Um
Blob e a implementação de rede podem criar cópias adicionais. As tarefas na fila mantêm
referências aos arquivos em vez de bytes de arquivos já lidos.
Criar um pequeno pool de workers
O pool acima tem dois workers e no máximo quatro tarefas pendentes. Esse é um limite de escalonamento, não uma recomendação baseada na quantidade de CPUs. Mais workers podem aumentar a sobrecarga de processamento, memória e requisições sem aumentar a vazão útil. Faça medições com sua própria etapa de processamento e seu receptor.
Receba os blocos localmente
Salve este código como server.mts. A extensão .mts mantém a execução inequivocamente no modo de
módulos ES, inclusive dentro de um projeto CommonJS. stripTypeScriptTypes()
remove as anotações; não faz a verificação de tipos dos scripts.
import { readFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { stripTypeScriptTypes } from 'node:module'
const CHUNK_SIZE = 1024 * 1024
const MAX_BODY = CHUNK_SIZE + 4096
const TTL_MS = 60_000
type Upload = { bytes: Buffer; received: number; complete: boolean; expires: number }
const uploads = new Map<string, Upload>()
class HttpError extends Error {
status: number
constructor(status: number, message: string) { super(message); this.status = status }
}
function expire(): void {
for (const [id, upload] of uploads) {
if (upload.expires <= Date.now()) uploads.delete(id)
}
}
async function readBody(req: IncomingMessage, limit: number): Promise<Buffer> {
const bytes = Buffer.alloc(limit)
let length = 0
// Keep the connection long enough to report an oversize body.
for await (const part of req.iterator({ destroyOnReturn: false })) {
if (!Buffer.isBuffer(part)) throw new HttpError(400, 'Expected bytes')
if (length + part.length > limit) throw new HttpError(413, 'Body too large')
part.copy(bytes, length)
length += part.length
}
return bytes.subarray(0, length)
}
async function main(): Promise<void> {
const assets = new Map<string, string>([['/', await readFile(new URL('./index.html', import.meta.url), 'utf8')]])
for (const name of ['main', 'upload-worker']) {
assets.set(`/${name}.js`, stripTypeScriptTypes(await readFile(new URL(`./${name}.ts`, import.meta.url), 'utf8')))
}
const port = Number(process.argv[2] ?? 0)
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Invalid port')
let origin = ''
let active = 0
async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
if (req.headers.host !== new URL(origin).host ||
(req.headers.origin !== undefined && req.headers.origin !== origin)) throw new HttpError(403, 'Use the printed local URL')
const method = req.method
if (method !== 'GET' && req.headers['x-upload-demo'] !== '1') throw new HttpError(403, 'Missing demo header')
const path = new URL(req.url ?? '/', origin).pathname
const isChunk = method === 'POST' && path.endsWith('/chunks')
const body = await readBody(req, isChunk ? MAX_BODY : 0)
// Parse before looking up an upload: pending work cannot retain a deleted session.
let chunk: Uint8Array | undefined
let offset: string | undefined
if (isChunk) {
const contentType = req.headers['content-type']
if (typeof contentType !== 'string' || !contentType.startsWith('multipart/form-data;')) throw new HttpError(400, 'Expected multipart data')
let form: FormData
try {
form = await new Request(origin, { method: 'POST', headers: { 'Content-Type': contentType }, body: new Uint8Array(body) }).formData()
} catch { throw new HttpError(400, 'Invalid multipart data') }
const value = form.get('chunk')
const position = form.get('offset')
if (!(value instanceof Blob) || value.size > CHUNK_SIZE || typeof position !== 'string' ||
!/^(0|[1-9]\d*)$/.test(position) || [...form.keys()].length !== 2) throw new HttpError(400, 'Invalid chunk fields')
chunk = new Uint8Array(await value.arrayBuffer())
offset = position
}
expire()
res.setHeader('Cache-Control', 'no-store')
res.setHeader('X-Content-Type-Options', 'nosniff')
const asset = assets.get(path)
if (method === 'GET' && asset !== undefined) {
res.setHeader('Content-Type', path === '/' ? 'text/html; charset=utf-8' : 'text/javascript')
res.end(asset)
return
}
const match = /^\/uploads\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})(?:\/(chunks|complete|file))?$/.exec(path)
if (!match) throw new HttpError(404, 'Unknown route')
const [, id, operation] = match
if (method === 'POST' && operation === undefined) {
const size = req.headers['upload-length']
if (typeof size !== 'string' || !/^(0|[1-9]\d*)$/.test(size) || Number(size) > 8 * CHUNK_SIZE) throw new HttpError(400, 'Expected a size from 0 to 8 MiB')
if (uploads.has(id)) throw new HttpError(409, 'Upload already exists')
if (uploads.size >= 4) throw new HttpError(503, 'Upload capacity reached')
uploads.set(id, { bytes: Buffer.alloc(Number(size)), received: 0, complete: false, expires: Date.now() + TTL_MS })
res.writeHead(201).end()
return
}
if (method === 'DELETE' && operation === undefined) {
uploads.delete(id)
res.writeHead(204).end()
return
}
const upload = uploads.get(id)
if (!upload) throw new HttpError(404, 'Upload missing or expired')
if (isChunk && operation === 'chunks') {
const expected = Math.min(CHUNK_SIZE, upload.bytes.length - upload.received)
if (!chunk || chunk.length === 0 || chunk.length !== expected || offset !== String(upload.received) || upload.complete) throw new HttpError(409, 'Unexpected chunk offset or length')
upload.bytes.set(chunk, upload.received)
upload.received += chunk.length
res.writeHead(204).end()
return
}
if (method === 'POST' && operation === 'complete') {
if (upload.received !== upload.bytes.length) throw new HttpError(409, 'Missing bytes')
upload.complete = true
res.end(String(upload.received))
return
}
if (method === 'GET' && operation === 'file') {
if (!upload.complete) throw new HttpError(409, 'Upload is not complete')
res.setHeader('Content-Type', 'application/octet-stream')
res.setHeader('Content-Disposition', 'attachment; filename="upload.bin"')
res.end(upload.bytes)
return
}
throw new HttpError(405, 'Unsupported operation')
}
const server = createServer({ requestTimeout: 10_000, headersTimeout: 10_000 }, (req, res) => {
if (active >= 6) { res.writeHead(503, { Connection: 'close' }).end('Too many requests'); return }
active++
let handled = false
let closed = false
const deadline = setTimeout(() => { req.destroy(); res.destroy() }, 10_000)
function release(): void {
if (handled && closed) { clearTimeout(deadline); active-- }
}
res.once('close', () => { closed = true; release() })
handle(req, res).catch((error: unknown) => {
const status = error instanceof HttpError ? error.status : 500
const message = error instanceof HttpError ? error.message : 'Request failed'
if (!res.destroyed) res.writeHead(status, { Connection: 'close' }).end(message)
}).finally(() => { handled = true; release() })
})
server.maxConnections = 16
await new Promise<void>((resolve, reject) => {
server.once('error', reject)
server.listen(port, '127.0.0.1', () => resolve())
})
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Missing server address')
origin = `http://127.0.0.1:${address.port}`
setInterval(expire, 1000).unref()
process.once('SIGINT', () => {
server.close()
server.closeAllConnections()
})
console.log(`Open ${origin}`)
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : 'Could not start the server')
process.exitCode = 1
})
Os quatro buffers de arquivos registrados somam no máximo 32 MiB. A análise das requisições usa buffers adicionais; cada corpo aceito é limitado a 1 MiB mais 4 KiB de sobrecarga multipart, incluindo dados finais. Há no máximo seis manipuladores ativos. Um download pode manter o buffer do arquivo após a exclusão ou expiração, portanto a vaga do manipulador permanece ocupada até que tanto o processamento quanto o encerramento da resposta terminem. Esses limites descrevem as operações da aplicação, não a memória total do processo nem o momento da coleta de lixo.
No diretório com os quatro arquivos salvos, execute este comando em primeiro plano:
node server.mts
Abra a URL http://127.0.0.1:PORT exibida. Selecione os arquivos e clique em
Upload files. Cada linha chega a
Confirming… após a confirmação dos seus blocos; somente uma
resposta de conclusão bem-sucedida produz Upload complete. e um link de download.
Clique no link para salvar os bytes recebidos como upload.bin pelo navegador. Uma contagem de
bytes correspondente não é uma soma de verificação de integridade; compare o arquivo baixado
com o original ao verificar este exemplo.
Pare o servidor com Ctrl+C, que encerra suas conexões. Para selecionar uma porta livre específica,
acrescente seu número a node server.mts.
Se a porta estiver ocupada ou faltar um arquivo-fonte, o processo termina com erro em vez de
exibir uma URL pronta para uso.
Compatibilidade com navegadores
Verifique nas versões de navegador que você suporta a disponibilidade de Worker, Blob.stream() e
AbortSignal.timeout().
Este exemplo não transfere um fluxo entre threads. Seu tempo limite de cinco segundos para
requisições no navegador usa o tempo ativo e pode pausar em um worker suspenso; o receptor tem um
prazo separado de dez segundos. Outros navegadores e sistemas operacionais não foram testados
neste tutorial.
Boas práticas de gerenciamento de memória
Mantenha um bloco da aplicação em trânsito por worker e uma pequena fila de referências a arquivos.
O gerador cancela um fluxo inacabado e libera o bloqueio do seu leitor quando uma requisição falha.
A página encerra todos os workers ao final de um lote, no cancelamento e em pagehide.
Clique em Cancel uploads enquanto um lote estiver pendente. As linhas ativas e na fila
recebem Upload canceled.; as linhas já concluídas mantêm
seus resultados. A página tenta excluir os IDs de uploads que falharam ou foram cancelados e
mantém os controles desabilitados até que a limpeza termine. Uma resposta de criação perdida,
o fechamento da página ou uma exclusão malsucedida podem deixar bytes armazenados até a expiração.
Encerrar um worker não executa seus blocos finally nem desfaz uma requisição já aceita pelo
receptor.
Segurança e resiliência
O servidor escuta em 127.0.0.1, verifica o host e a origem do navegador e exige um cabeçalho
personalizado para mutações. Ele não habilita acesso entre origens, não interpreta nomes de
arquivos como caminhos nem serve os bytes enviados como HTML executável. Esses são limites da
demonstração local, não autenticação ou validação de conteúdo. Não exponha este receptor como um
serviço público de upload.
Um serviço implantado precisa de autenticação, autorização, cotas por usuário, HTTPS, armazenamento durável e validação adequada aos seus consumidores. Este protocolo baseado em deslocamentos rejeita deliberadamente blocos duplicados ou fora de ordem. Adicionar novas tentativas exige um contrato de idempotência; a recuperação após recarregar a página exige estado persistido e reconciliação com o receptor.
Depurar Web Workers
Inspecione as requisições e mensagens dos workers nas ferramentas de desenvolvedor do navegador. Um erro ao carregar o script cancela o pool; uma falha HTTP se torna Upload failed. Try again. na linha correspondente. Uma resposta 409 indica um deslocamento incorreto, um tamanho de bloco incorreto ou um arquivo incompleto. Uma resposta 503 pode significar que todas as quatro vagas de upload estão ocupadas, incluindo arquivos concluídos; aguarde a expiração deles antes de tentar novamente. IDs ausentes ou expirados retornam 404.
Armadilhas comuns
- Tratar 100% dos bytes confirmados como conclusão. O último POST ainda pode falhar.
- Reabilitar a entrada antes que o trabalho cancelado termine. Esta página aguarda sua tentativa de limpeza.
- Supor que os tamanhos das leituras do fluxo correspondam aos tamanhos dos blocos HTTP. O agregador lida com um restante menor.
- Supor que encerrar um worker exclua o estado no servidor. A expiração continua necessária.
- Ler antecipadamente todos os arquivos na fila, o que anula o processamento incremental.
Principais conclusões
Experimente um arquivo vazio e um arquivo binário um pouco maior que 1 MiB, depois compare cada resultado baixado com sua entrada. O escopo útil é o fluxo completo, desde uma leitura incremental até um bloco reconhecido pelo receptor e um arquivo confirmado, com um pequeno número de tarefas ativas. Para uploads retomáveis em produção, comece com um protocolo com manutenção ativa, como tus, e um receptor compatível. O plugin Tus do Uppy fornece um cliente para navegador com manutenção ativa.
