Quando dá errado¶
Como usar esta página
Cada entrada começa pela mensagem literal que aparece no terminal ou no console, ou pelo sintoma quando não há mensagem nenhuma. Se você chegou aqui com um erro na tela, cole um pedaço dele na busca do site (a lupa no topo) — a entrada certa aparece.
As mensagens desta página são verificadas contra o código-fonte por um teste automatizado, então elas não envelhecem em silêncio.
Falta um extra¶
De longe a categoria mais comum, e a mais fácil de resolver: o tempestweb instala enxuto. Cada capacidade pesada mora num extra, e a mensagem sempre diz qual.
| Mensagem contém | Instale |
|---|---|
serving Mode B needs the 'server' extra (FastAPI + uvicorn) |
pip install "tempestweb[server]" |
the dev server needs the 'server' extra |
pip install "tempestweb[server]" |
the dev watcher needs watchfiles |
pip install "tempestweb[cli]" |
tomlkit is required for tempestweb sync |
pip install "tempestweb[cli]" |
PyJWT is required for verify_jwt |
pip install "tempestweb[auth]" |
redis is required for RedisSessionRouter |
pip install "tempestweb[server]" + redis |
pywebpush is required to send WebPush |
pip install "tempestweb[webpush]" |
cryptography is required to generate VAPID keys |
pip install "tempestweb[webpush]" |
FastAPI is required for webpush_router |
pip install "tempestweb[server]" |
Na dúvida, leia a própria mensagem
Toda mensagem dessa família termina com o comando exato. Elas foram escritas para serem a documentação — não é preciso procurar em lugar nenhum.
Capacidades nativas¶
no native bridge installed (off-platform, or bootstrap incomplete)¶
Um await native.<capacidade>() rodou onde não existe browser do outro lado da
ponte. Três causas, em ordem de frequência:
- Você está num teste ou num script, fora de uma sessão. Não há browser — injete um duplo, ou mova a chamada para dentro de um handler.
- O bootstrap do Modo A não completou. O
bootstrap.jsgerado instala a ponte antes de chamar obootstrap()Python; se a página quebrou antes disso, o erro aparece na primeira interação. - A sessão do Modo B foi fechada e um handler ainda em voo tentou usar a ponte.
Referência: Capacidades nativas.
the installed native bridge does not support the event channel¶
Você chamou um watch() / listen() (geolocalização contínua, rede, sensores)
numa ponte que só resolve chamadas de uma vez. No Modo A isso acontece quando o
bootstrap() recebeu dispatch mas não recebeu subscribe/unsubscribe — o
que dá esta variante mais específica:
O bootstrap.js gerado pelo tempestweb build passa os três. Se você monta o
bootstrap à mão, passe também os dois de streaming.
Referência: Canal de eventos nativo.
Sessão e handlers¶
A interface travou — nenhum botão responde¶
Sem erro nenhum, a tela simplesmente para. A sessão despacha um evento por
vez: enquanto um handler roda, nada mais é lido. Um await demorado dentro do
handler — inferência de modelo, API externa lenta, arquivo grande — congela a
conexão inteira daquele usuário, e nem um "Cancelar" adianta (o clique entra na
fila atrás do trabalho que deveria interromper).
Tire o trabalho do handler com spawn:
from tempestweb.runtime import spawn
async def analisar(app: App[State]) -> None:
app.set_state(lambda s: setattr(s, "status", "processando…"))
async def trabalho() -> None:
resultado = await algo_demorado()
app.set_state(lambda s: setattr(s, "resultado", resultado))
spawn(trabalho())
Referência: Trabalho longo: o dispatch é serial.
spawn() needs a running tempestweb session¶
spawn() needs a running tempestweb session; call it from an event handler, or await the coroutine directly
spawn pendura a task na sessão que está no contexto, e não há sessão nenhuma
no contexto de onde você chamou — tipicamente um teste, um script, ou código de
módulo que roda no import. Dentro de um handler sempre há. Fora dele, só
await a corrotina direto.
O handler roda, mas a tela não muda¶
Sem erro. Quase sempre é mutação de estado fora de um set_state:
# ❌ o estado muda, mas nada reconstrói a árvore
async def marcar(app: App[State]) -> None:
app.state.feito = True
# ✅
async def marcar(app: App[State]) -> None:
app.set_state(lambda s: setattr(s, "feito", True))
O repaint é agendado pelo set_state, não pela mudança do objeto.
Build e Modo C¶
TranspileError com file:line¶
O compilador do Modo C aceita um subconjunto de Python tipado, e recusa cedo com a linha exata. Os mais frequentes:
import x funciona para os módulos que o Modo C serve (re, json, math,
base64, asyncio) e para nada além disso — a mensagem de um módulo recusado
diz o que fazer no lugar (datetime → formate no estado e passe a string).
E o que mais aparece ao portar um app já pronto:
O Modo C enxerga tempest_core, tempestweb.components e tempestweb.native
— este último nas três formas: from tempestweb import native,
from tempestweb.native import storage e
from tempestweb.native.geolocation import get_position. Já import
tempestweb.native não: a mensagem diz qual forma escrever no lugar.
Import de stdlib só de anotação (collections.abc, typing) também passa: o
nome existe para o type-checker e não custa import nenhum no JS — mas usá-lo como
valor é erro ('Any' is a type-only name), porque nada o importaria.
Fora dessa lista, tempestweb.presets e tempestweb.observability não são
alcançáveis: telas montadas com presets rodam nos Modos A e B, não em C.
Capacidade nativa que o Modo C não tem em processo (camera) é recusada
dizendo qual modo a tem:
`camera` is not served in Mode C: the facade in `native.js` has no `camera`,
so the capability needs Mode A (Pyodide) or Mode B (server)
Nome legal em módulo legal ainda pode faltar no cliente — aí o erro cita o nome, não o módulo:
Referência: Modo C — transpile.
A lista virtualizada ficou vazia e não volta¶
Uma janela deslizada fundo contra uma lista que encurta — pull-to-refresh que
volta para a primeira página, filtro que reduz o resultado, remoção em massa —
resolvia para vazio: o _resolve_window do core prende o start na contagem, e
[45, 75) contra 25 itens vira [25, 25), zero linha. Sem linha não há scroll, e
sem scroll não há evento que reposicione a janela: a lista fica travada vazia com
os dados carregados no estado.
Desde 0.97.0 o controlador de virtualização se recupera: depois de cada lote de patches, uma lista com itens que não materializou nenhum (ou cuja janela começa além da última página) pede a última página — que é o fim da lista, se ela ainda for maior que a janela, e o topo quando ela couber inteira.
A regra de resolução continua no core
Isto é rede de segurança, não a correção da regra: _resolve_window ainda
prende o start na contagem. Quem quiser que uma janela encurtada resolva
para a última página em vez de vazio precisa mudar lá — e aí os três modos
mudam de uma vez.
O handler recebe o evento no lugar do valor que capturou¶
O idioma clássico de captura em laço — parâmetro com default, para fugir do late binding do Python — recebia o objeto de evento em vez do índice:
for index, item in enumerate(items):
def toggle(i: int = index) -> None: # o idioma
select(i)
Accordion(..., on_toggle=toggle)
A convenção de chamada era decidida pela espécie do parâmetro, nunca por ele
ter default — e um parâmetro com default não é algo que o chamador precise
fornecer. Resultado medido no examples/faq-accordion: open_index virava um
ClickEvent e o acordeão parava de responder de vez.
Corrigido em 0.96.0 nos três modos: o handler só recebe o evento quando
declara um parâmetro sem default (ou *args). Em Modo C a pergunta é a mesma e
sai de graça — fn.length conta os parâmetros antes do primeiro default.
Junto, o Modo C passou a emitir o default: def toggle(i=index) saía
(i) => …, então a captura sumia e o closure respondia undefined.
O campo não aceita digitação, ou a grade rende uma coluna¶
Três widgets declarados que o renderizador desenhava como div anônimo — nos
três modos, porque client/dom.js é compartilhado:
TextAreavirava umdivcom a cara de campo (a folha base estiliza por[data-tw-type]) e sem nada para focar. Corrigido em 0.94.0: vira<textarea>, comrowsemaxlength.MaskedInputvirava um retângulo morto — CPF, telefone e CEP não existiam. Agora é<input>com a máscara aplicada enquanto se digita (9dígito,Aletra, o resto literal), e o cursor fica onde o leitor deixou.LazyGrid.columnsera declarado e nunca lido: uma galeria de 3 colunas rendia 1 item por linha. Agora viradisplay: grid+grid-template-columns: repeat(N, minmax(0, 1fr)), e a reserva de espaço da virtualização passou a contar linhas, não itens.
Junto veio o motivo de o campo mascarado engolir tudo mesmo depois de virar
<input>: o builder do Modo C mapeava o on_change desses widgets para
click. A lista de "controle de formulário de verdade" era escrita à mão ao
lado do gerador e drifta em silêncio; agora é derivada da tabela de tags do
renderizador. Isso conserta também o PinInput, que tinha o mesmo defeito.
Se você vê isso, atualize:
O switch não alterna, o slider não arrasta, o seletor de data não abre¶
O mesmo defeito da entrada acima, nos onze widgets que faltavam — a auditoria
que a #130 pediu, feita na #143. Switch, Slider, RangeSlider, Dropdown,
Autocomplete, DatePicker, TimePicker, FilePicker e TabBar viravam div
anônimo: sem controle para operar e sem evento para reportar, nos três modos.
Corrigido em 0.98.0 — cada um vira o controle nativo equivalente (veja Controles). Três detalhes que vinham no mesmo pacote:
- O
Checkboxjá existia e oon_changedele chegava como dicionário cru. O cliente reportava{"value": "on"}, que não valida comoToggleEvent(checked)—event.checkedera umAttributeErroresperando o primeiro clique. O payload passou a ter a forma do widget. TabVieweRouteDrawercontinuamdiv, por decisão. Os dois têm filho de IR, e filho criado pelo renderizador só é legal dentro de folha da IR. Quem desenha a faixa de abas é oTabBar, ao lado; oopendoRouteDrawerviroudata-tw-open, que a folha base usa para deslizar a gaveta.- O
Switchera um quadrado, oSlidertinha 4px de altura. O Style que o core resolve para esses widgets descreve as peças que um renderizador de desenho à mão pinta (a caixa, o knob, a trilha), e style inline ganha da folha base. Agora a geometria de peça é descartada e a cor resolvida viraaccent-color.
Dark mode não muda nada em Modo C¶
O app chama app.set_theme(Theme(mode=ThemeMode.DARK)), o Modo B escurece e o
mesmo artefato transpilado continua claro. Duas causas, as duas corrigidas na
0.99.0:
- As tabelas de estilo geradas não tinham eixo de modo. O Modo C não tem Python, então o estilo resolvido de cada widget viaja em tabela gerada — e ela era gerada com o tema default. Como o estilo inline ganha do stylesheet, era a metade com precedência que renderizava claro.
- O builder recusava o kwarg
theme. Não havia como nem pedir o modo escuro:Button(theme=app.theme)compilava para um builder que não nomeavatheme, então o Modo C ignorava e os Modos A/B resolviam certo — a mesmaviewcom dois resultados.
Passe o tema ao widget
O tema é campo do widget, não ambiente: Button(label="x",
theme=app.theme). Sem isso, o widget resolve a paleta clara nos três modos —
é a regra do core, não um detalhe do Modo C. Veja
Tema.
A folha base continua clara
O fundo do Input, o fundo da página e os estados de hover/foco vêm dos
tokens --tw-*, que não têm eixo de modo — num app escuro o campo aparece
branco. Rastreado em
#148.
setattr is not defined (Modo C)¶
setattr(obj, nome, valor) só era portado na forma lambda s: setattr(s, "campo", v)
com nome constante. Com nome dinâmico — dentro de um def mutate(...) — saía
uma chamada a um setattr que não existe. Medido no examples/br-cadastro, cujo
bloco de endereço inteiro era inerte. Corrigido em 0.94.0, junto com getattr.
object is not iterable ou X.pop is not a function (Modo C)¶
Um clique morre no console e a tela não muda. É um dict sendo tratado como lista.
dict(outro)compilava paraObject.fromEntries(outro), que exige um iterável de pares e explode num mapeamento. O compilador não sabe qual dos dois você tem —dict(pares)também é legítimo — então desde 0.93.0 a decisão é em runtime.d.pop(chave, default)caía nopopde array, que num objeto não existe.
Medidos no examples/form, cujo submit morria seis vezes por clique com a página
renderizada e o formulário inerte.
_pattern.match is not a function (Modo C)¶
O validador com re.compile(...) morre, mas o mesmo código com atribuição sem
anotação funciona.
O compilador rastreia qual nome guarda um padrão compilado — é isso que deixa
.match() virar o helper certo sem sequestrar um .match() alheio — mas só
rastreava a forma sem anotação. _pattern: re.Pattern[str] = re.compile(…),
que é a forma que as regras de estilo deste repo pedem, perdia o rastro e emitia
.match cru num RegExp, que não tem esse método. Corrigido em 0.93.0; vale
também para form: Form = Form(…).
c.isupper is not a function (Modo C)¶
A tabela de predicados de str do Modo C tinha isdigit/isalpha/isalnum/
isspace e faltavam os de caixa. Adicionados em 0.93.0, com a semântica do
Python: exige ao menos um caractere com caixa, então "1".isupper() é False.
Theme.from_seed is not a function (Modo C)¶
Página em branco, um erro só no console, e o build tinha passado.
_served.py responde "o cliente exporta esse nome?" — não responde "esse nome
tem esse método?". Theme é servido (o do Modo C carrega o modo), mas a
paleta Material 3 semeada não é portada: quem pinta os tokens é a folha de
estilo base.
Desde 0.92.0 isso é erro de compilação com arquivo:linha:
O manifesto de membros (tempestweb/transpile/_members.py) é gerado
introspectando o cliente no Node, que é a única fonte honesta — o JS é o que o
browser carrega. Color.from_hex, Edge.all e Edge.symmetric continuam
passando, porque esses o cliente carrega de verdade.
Invalid left-hand side in assignment (Modo C)¶
Um clique não faz nada e o console mostra isso. É xs[:] = [...].
Uma fatia lê como .slice(...), então a atribuição saía
xs.slice(0) = [...] — que parseia, e por isso o node --check do build
passava. Corrigido em 0.92.0: vira xs.splice(0, xs.length, ...novo), que é a
substituição no lugar que o Python faz. Fatia parcial (xs[1:3] = …) é recusada
no build, porque ela pode crescer ou encolher a lista.
O on_change do componente não dispara (Modo C)¶
O componente aparece, o texto que você digita fica na caixa, e o handler nunca roda — clicar em "Entrar" não faz nada.
Os props de widget viajam em camelCase no builder gerado (on_submit vira
onSubmit), e a renomeação era decidida resolvendo o nome no tempest_core.
Um componente que só existe no facade — LoginForm, SignupForm, TextField,
EmailField, PasswordField — não resolvia lá, então os props saíam no
snake_case do fio e o builder, que desestrutura camelCase, descartava
todos os handlers em silêncio.
Corrigido em 0.90.0 — o nome é procurado no tempest_core e depois em
tempestweb.components. Como efeito colateral bom, o kwarg desconhecido volta
a ser recusado no build: LoginForm(subtitle="x") agora falha com
arquivo:linha.
Color.from_hex is not a function / Class constructor X cannot be invoked without 'new'¶
Página em branco, um erro só no console, e o build tinha passado.
Color.from_hex: no core,Coloré um modelo com o classmethodfrom_hex— o jeito de escrever cor literal (65 chamadas nos exemplos). O Modo C exportava só a fábrica, então a chamada compilava e morria na montagem. Portado em 0.90.0.field(default_factory=OutraDataclass): dataclass compila para classe JS, e chamar classe semnewéTypeErrorduro. O default aninhado saía(Address)()e o app morria no primeiromakeState(). Corrigido em 0.90.0.
Os dois são a mesma família do Edge que não era chamável (0.86.0): valor do
core cujo helper faltava no cliente. O guard de build roda node --check, que
faz parse sem executar — por isso passavam.
O campo com mensagem de erro não fica vermelho (Modo C)¶
O Input mostra a mensagem embaixo, mas a borda e o texto continuam na cor
normal — em Modo A ou B, o mesmo código pinta os dois de vermelho.
Um campo com error preenchido está inválido, e o core repinta a borda e o
texto no papel error na hora de construir. Essa regra mora no estilo
construído, não na folha de estilo, então o builder do Modo C, que é
passthrough, a perdia em silêncio: o campo compilava, montava e mentia.
Corrigido em 0.88.0 — Input resolve por resolveFieldStyle, que aplica a
regra do core (borda de 1px no papel error, SideBorder só embaixo quando o
field_variant é flushed, e o style do chamador ainda ganha por último).
Se você vê isso, atualize o pacote:
O app carrega com a versão antiga do código¶
Nenhum erro, nenhum aviso: você reconstruiu, recarregou, e a correção não está lá. É o service worker.
Por decisão de projeto o worker não chama skipWaiting — quem controla a
atualização é a página, com um prompt para o usuário. A consequência em
desenvolvimento é que, depois de um build, o worker novo instala mas fica
esperando, e o antigo continua servindo o app-shell do cache dele. Um F5
comum não troca: a aba segue controlada pelo worker velho.
Diagnóstico e correção, no Chrome DevTools → Application → Service Workers:
- Se aparecer um worker com o rótulo waiting to activate, é isso.
- Marque Update on reload enquanto estiver desenvolvendo — cada reload passa a ativar o worker novo.
- Para limpar de vez: Unregister, e em Application → Storage use Clear site data, então recarregue.
Em produção o caminho é o outro: a página detecta o worker esperando e oferece o
prompt de atualização, que envia {type:"SKIP_WAITING"}.
Referência: PWA e offline.
Render e patches¶
patch path out of range — e a tela fica faltando pedaço¶
RangeError: tempestweb: patch path out of range at index 1 (path [0, 1],
step 1): div[data-tw-key="appbar-actions"] has 1 children [button[data-tw-key="…"]]
O Python calculou um patch endereçando um nó que o cliente não tem. Um patch é uma caminhada por índices de filho, então quando um passo não resolve o lote para ali: a tela fica com o que já tinha, faltando exatamente o que o resto do lote carregava — um botão, uma coluna de tabela, um campo de formulário.
O sintoma é traiçoeiro porque não parece erro: a tela renderiza, só que incompleta. Só o console reclama.
O que a mensagem te dá. Ela nomeia o path inteiro, qual passo falhou, qual
nó o cliente tem ali (pelo data-tw-key, o mesmo identificador da IR) e quantos
filhos ele de fato tem. Compare com a árvore que o seu view() constrói: se o
pai tem menos filhos do que deveria, algum lote anterior não chegou.
Como investigar. Ligue o log do stream de patches pelo console — a flag é lida a cada lote, então funciona numa página que já está com problema:
A partir daí cada lote sai numerado no console, e o lote que falhar vem acompanhado de um outline da árvore que o cliente tem.
O cliente se repara sozinho
Quando um lote não aplica, o cliente pede um resync e o Python responde
com a scene inteira num Replace de raiz. Vale nos três modos — o Modo A
ganhou isso na 0.102.0; antes dela, um patch que falhasse deixava a tela
truncada até o reload.
Conexão (Modo B)¶
websocket disconnected / sse transport is closed¶
O cliente sumiu (aba fechada, rede caiu, proxy cortou) e algo tentou escrever no transporte depois disso. Como erro de servidor é esperado e não exige ação. Se acontece o tempo todo em produção com usuários ativos, olhe o reverse proxy: timeout de idle curto ou falta do upgrade de WebSocket derruba conexões saudáveis.
Se a sua infraestrutura simplesmente não deixa WebSocket passar, troque o shell por SSE — está em Deploy.
O editor não completa nada de tempestweb¶
O mypy trata tudo como Any e o autocomplete não sugere nada. Isso é sintoma de
uma versão anterior à 0.64.0, que não trazia o marcador py.typed — sem
ele, a PEP 561 manda o verificador ignorar os tipos, por mais anotado que o
pacote esteja.
Desenvolvendo o tempestweb¶
MODULE_NOT_FOUND ao rodar os testes de cliente¶
A forma com diretório quebra no Node 24+:
Use sempre o glob, entre aspas para o shell não expandir antes.
Recapitulando¶
- Mensagem de extra faltando já contém o comando — leia-a antes de procurar aqui.
- Ponte nativa ausente significa "não há browser deste lado": teste, script ou bootstrap incompleto.
- Interface travada sem erro é o dispatch serial; a resposta é
spawn. patch path out of rangeé árvore do cliente divergindo da do Python; a mensagem diz qual nó, e__tempestweb_debugmostra o stream inteiro.- Tela que não muda é mutação sem
set_state. - Código velho depois do build é o service worker esperando; ligue Update on reload durante o desenvolvimento.
- Não achou aqui? Veja o FAQ ou abra uma issue.