Ir para o conteúdo

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)

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:

  1. 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.
  2. O bootstrap do Modo A não completou. O bootstrap.js gerado instala a ponte antes de chamar o bootstrap() Python; se a página quebrou antes disso, o erro aparece na primeira interação.
  3. 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

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:

mode A native event channel is not wired (no subscribe callable)

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:

is not available in Mode C

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).

variadic parameters (*args / **kwargs) are not supported
function decorators are not supported

E o que mais aparece ao portar um app já pronto:

is not supported (only tempest_core, `tempestweb.components` and `tempestweb.native`)

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:

is not available in Mode C (the transpile client exports no such name)

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.

uv add "tempestweb>=0.97.0"

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.

uv add "tempestweb>=0.96.0"

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:

  • TextArea virava um div com a cara de campo (a folha base estiliza por [data-tw-type]) e sem nada para focar. Corrigido em 0.94.0: vira <textarea>, com rows e maxlength.
  • MaskedInput virava um retângulo morto — CPF, telefone e CEP não existiam. Agora é <input> com a máscara aplicada enquanto se digita (9 dígito, A letra, o resto literal), e o cursor fica onde o leitor deixou.
  • LazyGrid.columns era declarado e nunca lido: uma galeria de 3 colunas rendia 1 item por linha. Agora vira display: 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:

uv add "tempestweb>=0.94.0"

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 Checkbox já existia e o on_change dele chegava como dicionário cru. O cliente reportava {"value": "on"}, que não valida como ToggleEvent(checked)event.checked era um AttributeError esperando o primeiro clique. O payload passou a ter a forma do widget.
  • TabView e RouteDrawer continuam div, 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 é o TabBar, ao lado; o open do RouteDrawer virou data-tw-open, que a folha base usa para deslizar a gaveta.
  • O Switch era um quadrado, o Slider tinha 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 vira accent-color.
uv add "tempestweb>=0.98.0"

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 nomeava theme, então o Modo C ignorava e os Modos A/B resolviam certo — a mesma view com dois resultados.
uv add "tempestweb>=0.99.0"

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 para Object.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 no pop de 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:

the client's own object carries no such member

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 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.

uv add "tempestweb>=0.90.0"

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 classmethod from_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 sem new é TypeError duro. O default aninhado saía (Address)() e o app morria no primeiro makeState(). 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:

uv add "tempestweb>=0.88.0"

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 → ApplicationService Workers:

  1. Se aparecer um worker com o rótulo waiting to activate, é isso.
  2. Marque Update on reload enquanto estiver desenvolvendo — cada reload passa a ativar o worker novo.
  3. 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:

globalThis.__tempestweb_debug = true;

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

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.

pip install --upgrade tempestweb

Desenvolvendo o tempestweb

MODULE_NOT_FOUND ao rodar os testes de cliente

A forma com diretório quebra no Node 24+:

node --test tests/client/        # ❌ MODULE_NOT_FOUND
node --test "tests/client/*.test.js"   # ✅

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_debug mostra 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.