Suponha que Alice pertença ao workspace alpha. Ela solicita /workspaces/alpha/documents/doc-b/ e recebe um documento de beta. O servidor reconheceu Alice e conferiu sua participação no workspace. As duas verificações passaram. Onde a requisição saiu do caminho?
O exemplo didático usa documentos e identidades fictícios. A execução exige Node.js 22 ou posterior; o método de leitura vale para outras linguagens e frameworks. A pergunta útil é específica: qual workspace foi verificado e qual restringiu a consulta do documento?
Comece com uma requisição e a regra que deve cumprir
Mantenha juntos método, caminho, identidade e resultado esperado:
GET /workspaces/alpha/documents/doc-b/
Demo identity: alice, a member of alpha only
Stored document: doc-b belongs to beta
Expected: 404, with no document in the response
Broken result: 200, containing Beta release notes
A política do exemplo permite que membros acessem todos os documentos do workspace. Não há compartilhamentos individuais nem exceções de administrador. Para um membro de alpha, um documento fora de alpha recebe o mesmo status e corpo de um documento ausente. Pedir um workspace do qual não participa retorna 403 antes da chamada ao armazenamento.
Há duas relações a seguir: o usuário autenticado deve pertencer ao workspace da URL, e o documento retornado deve pertencer ao mesmo workspace. Verificar a primeira com sucesso ainda deixa a segunda para a consulta de dados.
Em um relato real, registre também a versão da aplicação ou do código. A rota na sua cópia precisa corresponder ao código que processou a requisição. Use uma fixture sintética para reproduzir localmente.
Encontre o registro da rota antes de abrir o manipulador
Baixe request-flow.mjs e request-flow.test.mjs na mesma pasta. Busque nomes específicos do caminho, como workspaces e documents, no editor. Com ripgrep, estes comandos restringem os próximos passos:
rg -n 'workspaces|documents' request-flow.mjs
rg -n 'middleware|runMiddleware|requireWorkspaceMember|findById|findInWorkspace' request-flow.mjs
Abra createApp. O objeto de rota reúne método, padrão de caminho, middleware e manipulador selecionado:
const route = {
method: 'GET',
pattern: /^\/workspaces\/([a-z0-9-]+)\/documents\/([a-z0-9-]+)\/$/,
middleware: [authenticate, requireWorkspaceMember],
handler: lookup === 'unscoped' ? getDocumentUnsafe : getDocument,
};
Siga também as linhas seguintes. handle compara new URL(request.url).pathname, verifica o método e atribui os segmentos capturados a workspaceId e documentId. Parâmetros de consulta não fornecem esses valores. O exemplo aceita letras minúsculas, dígitos e hífens e exige a barra final mostrada; não é um roteador genérico.
Em um framework, o caminho completo pode ser dividido entre montagem pai, como /workspaces/:workspaceId, e rota filha, como /documents/:documentId. Leia a montagem e confirme como os parâmetros chegam à filha. Uma ocorrência no manipulador não mostra quais middlewares envolvem seu chamador.
Leia os middlewares na ordem real
runMiddleware usa reduceRight para envolver o manipulador. Com [authenticate, requireWorkspaceMember], executa primeiro autenticação, depois participação e então o manipulador, conforme cada etapa chama next(). Os retornos de return next() chegam a handle, cujo catch converte um HttpError lançado em resposta.
Request → path and method match → authenticate
→ requireWorkspaceMember → document lookup → JSON Response
↘ thrown error → handle's catch → error Response
authenticate procura o cabeçalho Authorization em um mapa fixo e coloca o resultado em ctx.user. demo-alice e demo-bob são entradas públicas da fixture que representam uma sessão verificada. Isso basta para seguir a origem da identidade no exemplo; a verificação real de sessão ou token exige leitura separada.
Agora abra o middleware de participação:
async function requireWorkspaceMember(ctx, next) {
const { workspaceId } = ctx.params;
const allowed = ctx.user.workspaceIds.includes(workspaceId);
ctx.trace.push(`membership:${workspaceId}:${allowed ? 'allowed' : 'denied'}`);
if (!allowed) throw new HttpError(403, 'Forbidden');
return next();
}
Para Alice e um caminho com alpha, allowed é true. Nada nesta função examina documentId nem carrega documentos. Anote precisamente: “Alice pode entrar na rota alpha”. Escrever apenas “autorizado” facilita atribuir à verificação mais do que ela faz.
Marque também onde a execução para. Identidade ausente ou desconhecida produz 401 antes da participação. Um não membro recebe 403 antes do manipulador. Se o middleware chama next() antes de decidir, o manipulador pode já ter executado; a posição da chamada importa tanto quanto o nome do middleware.
Separe usuário, workspace solicitado e documento armazenado
Para a requisição que falha, uma nota útil cabe em quatro linhas:
| Valor | Origem | Valor nesta requisição | Onde seguir |
|---|---|---|---|
| Usuário e participações | authenticate lê o mapa fictício |
Alice, ['alpha'] |
requireWorkspaceMember |
| Workspace solicitado | Primeira captura da rota | alpha |
Verificação de participação e argumentos de consulta |
| Documento solicitado | Segunda captura da rota | doc-b |
Consulta do documento |
| Workspace real do documento | Registro armazenado | beta |
Predicado da consulta e resposta |
Adicione arquivo e linha ao examinar produtores e consumidores. Em uma aplicação maior, podem ser tipos em pacotes diferentes, mas a distinção permanece: a URL expressa o pedido, a identidade fornece participações e o registro fornece a propriedade.
Os dois IDs chegam juntos. Confira se saem juntos do manipulador.
Abra a consulta, mesmo quando o manipulador parece comum
O manipulador deliberadamente inseguro é:
async function getDocumentUnsafe(ctx) {
ctx.trace.push(`lookup:unscoped:${ctx.params.documentId}`);
const document = await ctx.store.findById(ctx.params.documentId);
if (!document) throw new HttpError(404, 'Not found');
return json(200, document);
}
Só documentId chega ao armazenamento. Siga findById até createStore: seu predicado é document.id === documentId. Não há comparação de workspace nem verificação posterior antes da serialização. O middleware aceitou alpha; a consulta pode retornar registros de qualquer workspace.
O erro não exige corrida nem sessão falsificada. Alice mantém a própria identidade e o caminho permitido alpha, fornecendo o ID fictício doc-b. A aplicação verifica um escopo e lê outro.
Em um projeto real, o nome da função seria evidência fraca. Um método de repositório poderia adicionar filtro de tenant, herdar sessão de banco com escopo ou depender de políticas por linha. Leia a implementação selecionada e sua configuração antes de concluir que um argumento omitido deixa a consulta sem escopo. Aqui, o predicado em memória é toda a consulta e pode ser conferido diretamente.
Execute o contraexemplo pela cadeia completa
Na pasta com os dois downloads, execute:
node request-flow.mjs
node --test request-flow.test.mjs
Não é preciso instalar, ter conta ou serviço. makeRequest constrói um Request Web, passado pelos testes a handle, que retorna um Response Web. A URL example.invalid é apenas analisada em memória. O exemplo nunca chama fetch nem abre socket de escuta.
A demonstração imprime estas respostas, seguidas das etapas visitadas e chamadas ao armazenamento:
normal: 200 {"id":"doc-a","workspaceId":"alpha","title":"Alpha release notes"}
reproduce: 200 {"id":"doc-b","workspaceId":"beta","title":"Beta release notes"}
fixed: 404 {"error":"Not found"}
Para a requisição quebrada, as etapas são:
request:GET -> route:alpha:doc-b -> authenticate:alice -> membership:alpha:allowed -> lookup:unscoped:doc-b -> response:200
O array de trace é instrumentação da fixture, fora da resposta. O teste também confere o documento e a chamada registrada a findById('doc-b'). As asserções conectam a sequência de ramificações aos dados efetivamente retornados.
O teste chamado reproduce: passa quando a implementação insegura retorna o documento de beta. O resultado verde confirma a reprodução do defeito. Outro teste exige que a implementação corrigida retorne 404 para o mesmo pedido.
Leve o workspace verificado até a consulta
O manipulador corrigido preserva ambos os valores da rota:
async function getDocument(ctx) {
const { workspaceId, documentId } = ctx.params;
ctx.trace.push(`lookup:scoped:${workspaceId}:${documentId}`);
const document = await ctx.store.findInWorkspace(workspaceId, documentId);
if (!document) throw new HttpError(404, 'Not found');
return json(200, document);
}
Dentro de findInWorkspace, o predicado exige document.workspaceId === workspaceId e document.id === documentId. O middleware já verificou o mesmo workspaceId. A fixture não altera ctx.params entre as etapas, e documentos e participações ficam fixos durante a execução.
Para /workspaces/alpha/documents/doc-b/, a consulta agora não encontra nada. IDs ausentes e de outro workspace produzem 404 com {"error":"Not found"}. O mesmo status e corpo evitam revelar explicitamente qual caso ocorreu; os testes não afirmam nada sobre tempo ou outros canais laterais.
Conferir document.workspaceId após uma leitura global também impediria este vazamento. Uma consulta com escopo torna a restrição explícita na chamada e não retorna registro externo ao manipulador. Neste exemplo em memória, Array.find ainda examina registros para avaliar o predicado. As chamadas registradas mostram argumentos, não contagens de I/O de banco ou isolamento físico.
Ao mapear para SQL ou ORM, examine a condição emitida e os parâmetros e teste com a camada real. Revogação de participação, documentos movidos, compartilhamentos e outras rotas precisam de regras e verificações próprias.
Teste o sucesso e os pontos de parada obrigatórios
A suíte tem dez testes: uma reprodução intencional e nove verificações do app corrigido. Vários exercitam mais de uma entrada:
| Requisição ou condição | Resposta esperada | Comportamento do armazenamento |
|---|---|---|
Alice lê alpha/doc-a; Bob lê beta/doc-b |
200, documento correto |
Ambos os IDs chegam a findInWorkspace |
Alice lê alpha/doc-b |
404, sem documento |
Consulta restrita a alpha |
Alice lê alpha/missing |
Mesmo status e corpo da linha anterior | Nenhum documento encontrado |
| Identidade fictícia ausente ou desconhecida | 401 |
Sem consulta; participação não alcançada |
Alice lê beta/doc-b |
403 |
Sem consulta |
Alice lê alpha/doc-b?workspaceId=beta |
404 |
O parâmetro não substitui o escopo da rota |
| Caminho desconhecido; método errado em rota conhecida | 404; 405 com Allow: GET |
Sem consulta |
| Armazenamento lança erro inesperado | 500, corpo genérico |
Erro chega ao encapsulador de resposta |
Os caminhos abreviados expandem para /workspaces/{workspaceId}/documents/{documentId}/, com query string após a barra final. As duas leituras bem-sucedidas impedem uma “correção” que rejeite tudo. As asserções de ausência de chamada detectam rejeição só depois de acessar o armazenamento. Juntas, verificam mais que status finais.
Os testes cobrem tratamento no processo, ordem de middleware, argumentos de consulta e respostas JSON da rota. Não exercitam transporte HTTP, proxies reversos, identidade de produção ou banco. Ao aplicar o método, comece no adaptador HTTP real e confira se seleciona o manipulador e os middlewares rastreados.
Deixe uma nota que outra pessoa possa verificar
Escreva o resultado em torno da requisição e dos pontos exatos onde seus valores são usados:
Trigger: Alice + GET /workspaces/alpha/documents/doc-b/
Route: createApp captures alpha and doc-b.
Membership: requireWorkspaceMember allows Alice for alpha.
Old read: getDocumentUnsafe passes only doc-b to findById.
Observed: 200 returns a row whose workspaceId is beta.
Fix: getDocument passes alpha and doc-b to findInWorkspace.
Regression: same request returns 404; both valid member reads still return 200.
Scope: in-memory request pipeline; production adapter and storage are untested.
Anexe posições no código e comando de teste. Se um colega perguntar “essa rota já não tem autorização?”, a nota mostra o que foi verificado e onde o workspace deixou de ser usado.