Voltar para o Blog
Quest Log

HTTPRequest na Godot 4: Como Consumir uma API pela Internet (GET, POST e JSON)

Ilustração de um jogo feito na Godot trocando dados JSON com um servidor na internet

Aprenda a usar o nó HTTPRequest na Godot 4: requisição GET e POST, leitura de JSON, headers, tratamento de erro e o padrão com await, com exemplos testáveis.

Todo jogo que conversa com a internet na Godot passa pelo mesmo nó: o HTTPRequest. Ranking de pontuação, save na nuvem, lista de notícias na tela inicial, validação de código promocional, tudo isso é, por baixo, uma requisição HTTP que sai do jogo, chega a um servidor e volta com uma resposta, quase sempre em JSON. Este post é o alicerce dessa conversa: o que o nó HTTPRequest é, como fazer GET e POST, como ler e tratar a resposta sem travar o jogo e como não cair nas pegadinhas clássicas, como disparar duas requisições no mesmo nó. Os exemplos usam a API pública httpbin.org, que ecoa de volta o que você envia, então dá para testar tudo hoje, sem escrever uma linha de servidor.

O que é o nó HTTPRequest e como ele funciona

O HTTPRequest é um nó da Godot 4 construído em cima da classe HTTPClient. O HTTPClient é a peça de baixo nível, onde você mesmo gerencia conexão, envio e leitura por etapas. O nó HTTPRequest embrulha tudo isso numa interface de uma chamada só: você invoca request(), o nó cuida da conexão em segundo plano e, quando a resposta chega (ou a requisição falha), ele emite o sinal request_completed com tudo que você precisa.

Duas características definem como você vai programar com ele:

É assíncrono. A chamada a request() retorna imediatamente. O jogo continua rodando, o loop de física continua, a UI continua respondendo. A resposta chega depois, pelo sinal. Isso é ótimo (requisição de rede pode levar segundos, e segundos de tela congelada matam qualquer jogo), mas exige que você pense em dois tempos: o momento de pedir e o momento de receber.

Uma requisição por nó por vez. Cada nó HTTPRequest processa uma única requisição até ela terminar. Guarde essa regra, ela tem uma seção própria mais adiante.

Setup: o nó na cena e o sinal conectado

Há dois jeitos de ter um HTTPRequest disponível. O primeiro é pelo editor: adicione um nó filho do tipo HTTPRequest na sua cena e referencie com @onready. O segundo é criar por código, útil em autoloads e managers:

extends Node

# Opção 1: nó adicionado pelo editor como filho desta cena
@onready var http: HTTPRequest = $HTTPRequest

# Opção 2: criado por código (use uma ou outra, não as duas)
func _criar_por_codigo() -> void:
    var http_dinamico: HTTPRequest = HTTPRequest.new()
    add_child(http_dinamico)
    http_dinamico.request_completed.connect(_on_request_completed)

O detalhe que derruba iniciante: um nó criado com HTTPRequest.new() precisa entrar na árvore com add_child() antes de fazer qualquer requisição, porque é o processamento do nó na árvore que move a requisição adiante.

A conexão do sinal segue o padrão normal da engine, o mesmo que você usa em botões e timers. Se sinais ainda são nebulosos para você, o guia de sinais na Godot cobre a base antes de você seguir aqui.

func _ready() -> void:
    http.request_completed.connect(_on_request_completed)

GET básico: pedindo dados a uma API

Com o nó pronto e o sinal conectado, um GET é uma linha:

func buscar_dados() -> void:
    var erro: int = http.request("https://httpbin.org/get")
    if erro != OK:
        push_error("Não foi possível iniciar a requisição. Código: %d" % erro)

Repare que request() retorna um código de erro imediato. Ele não diz nada sobre a resposta do servidor (que ainda não existe), só se a requisição conseguiu ser iniciada. OK significa "foi disparada"; qualquer outra coisa significa que nem saiu do lugar, por exemplo porque o nó já estava ocupado.

A resposta chega no callback, e a assinatura dele é fixa. Vale digitar com tipagem completa para o autocomplete trabalhar a seu favor:

func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
    if result != HTTPRequest.RESULT_SUCCESS:
        push_error("Falha de rede. Result: %d" % result)
        return

    if response_code != 200:
        push_error("Servidor respondeu com status %d" % response_code)
        return

    var texto: String = body.get_string_from_utf8()
    print(texto)

Os quatro parâmetros contam a história completa:

  • result é o veredito da camada de rede. HTTPRequest.RESULT_SUCCESS quer dizer que a conexão aconteceu e uma resposta chegou. Outros valores indicam DNS que não resolveu, conexão recusada, timeout.
  • response_code é o status HTTP do servidor: 200 para sucesso, 404 para recurso que não existe, 500 para erro do lado de lá.
  • headers traz os headers da resposta, um por string.
  • body é o corpo cru, em bytes.

A ordem de checagem importa. Primeiro result, porque se a rede falhou o response_code vem como 0 e o body vem vazio. Depois response_code, porque um 404 chega com result de sucesso: a rede funcionou, o servidor respondeu, a resposta só não é a que você queria.

Transformando o corpo em JSON utilizável

APIs falam JSON, e o corpo chega como PackedByteArray. A conversão tem dois passos: bytes para texto com get_string_from_utf8(), texto para dados com JSON.parse_string():

func _processar_corpo(body: PackedByteArray) -> void:
    var texto: String = body.get_string_from_utf8()
    var dados: Variant = JSON.parse_string(texto)

    if dados == null:
        push_error("Resposta não é um JSON válido")
        return

    var resposta: Dictionary = dados as Dictionary
    var origem: String = resposta.get("origin", "desconhecida")
    print("Requisição veio do IP: %s" % origem)

Dois cuidados aqui. JSON.parse_string() retorna Variant, e retorna null quando o texto não é JSON válido, então a checagem de null não é opcional: servidor fora do ar às vezes devolve uma página de erro em HTML, e o parse disso é null. E como o retorno é Variant, tipar com as Dictionary (ou as Array, conforme a API) devolve a você a tipagem estática no resto da função. Usar get() com valor padrão em vez de acesso direto por chave evita crash quando o servidor mudar o formato sem avisar, e servidor sempre muda o formato sem avisar.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

POST com JSON: enviando dados para a API

GET pede, POST envia. Para mandar JSON, três coisas mudam em relação ao GET: você declara o header Content-Type, serializa os dados com JSON.stringify() e informa o método HTTPClient.METHOD_POST:

func enviar_pontuacao(nome: String, pontos: int) -> void:
    var dados: Dictionary = {
        "jogador": nome,
        "pontuacao": pontos,
        "fase": "floresta-1"
    }
    var corpo: String = JSON.stringify(dados)
    var headers: PackedStringArray = ["Content-Type: application/json"]

    var erro: int = http.request(
        "https://httpbin.org/post",
        headers,
        HTTPClient.METHOD_POST,
        corpo
    )
    if erro != OK:
        push_error("Não foi possível iniciar o POST. Código: %d" % erro)

A assinatura completa de request() é request(url, custom_headers, method, request_data), e os três últimos são opcionais, por isso o GET lá de cima funcionou só com a URL. O header Content-Type: application/json avisa o servidor que o corpo é JSON; sem ele, muitos backends recusam a requisição ou interpretam o corpo errado. Se a sua API exigir autenticação, o token entra no mesmo array: "Authorization: Bearer seu-token". Nunca deixe tokens fixos no código do jogo, qualquer jogador consegue abrir o binário e ler.

O endpoint https://httpbin.org/post ecoa de volta o que recebeu: a resposta traz um campo json com exatamente o dicionário que você enviou. É perfeito para conferir se a serialização está certa antes de apontar para o seu backend de verdade.

Uma requisição por vez: o erro clássico e as três saídas

Chame request() duas vezes seguidas no mesmo nó e a segunda falha na hora, com a mensagem HTTPRequest is processing a request no console e um código de erro no retorno. Não é bug, é o contrato do nó: uma requisição por vez, do request() até o request_completed.

As três saídas, da mais simples à mais estrutural:

  1. Espere com await. Se as requisições são sequenciais por natureza (buscar o perfil, depois buscar o inventário), espere a primeira terminar antes de disparar a segunda. É o padrão da próxima seção.
  2. Um nó por requisição. Se as requisições são realmente simultâneas, crie um HTTPRequest para cada uma com HTTPRequest.new() e add_child(). Nós são baratos; você pode inclusive liberar com queue_free() depois da resposta.
  3. Fila. Num projeto maior, um manager com um Array de requisições pendentes despacha uma por vez: quando o request_completed chega, ele tira a próxima da fila. Dá controle de prioridade e evita bombardear o servidor.

O padrão com await: resposta no fluxo da função

Conectar sinal funciona sempre, mas espalha a lógica: o pedido fica numa função, a resposta em outra. Para fluxos lineares, o await deixa tudo no mesmo lugar. Você aguarda o próprio sinal, e ele devolve um Array com os mesmos quatro valores do callback:

func buscar_perfil() -> Dictionary:
    var erro: int = http.request("https://httpbin.org/get")
    if erro != OK:
        push_error("Falha ao iniciar a requisição. Código: %d" % erro)
        return {}

    var resposta: Array = await http.request_completed
    var result: int = resposta[0]
    var response_code: int = resposta[1]
    var body: PackedByteArray = resposta[3]

    if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
        push_error("Requisição falhou. Result: %d, status: %d" % [result, response_code])
        return {}

    var dados: Variant = JSON.parse_string(body.get_string_from_utf8())
    if dados == null:
        return {}
    return dados as Dictionary

O await pausa a função, não o jogo: enquanto a resposta não chega, tudo continua rodando. É o mesmo mecanismo que você já usa para esperar timers e animações, aplicado à rede.

Exemplo completo e testável

Juntando tudo: uma cena mínima que faz um GET e um POST contra o httpbin.org, com tratamento de erro e um estado de "carregando" para a UI reagir. Crie uma cena com um Node, anexe este script e rode:

extends Node

var carregando: bool = false
var http: HTTPRequest

func _ready() -> void:
    http = HTTPRequest.new()
    http.timeout = 10.0
    add_child(http)
    _testar_api()

func _testar_api() -> void:
    carregando = true

    var dados_get: Dictionary = await _requisitar("https://httpbin.org/get")
    if not dados_get.is_empty():
        print("GET ok. Origem: %s" % str(dados_get.get("origin", "?")))

    var pontuacao: Dictionary = {"jogador": "juan", "pontos": 1200}
    var dados_post: Dictionary = await _requisitar(
        "https://httpbin.org/post",
        HTTPClient.METHOD_POST,
        JSON.stringify(pontuacao)
    )
    if not dados_post.is_empty():
        var eco: Dictionary = dados_post.get("json", {}) as Dictionary
        print("POST ok. Servidor recebeu: %s" % str(eco))

    carregando = false

func _requisitar(url: String, metodo: int = HTTPClient.METHOD_GET, corpo: String = "") -> Dictionary:
    var headers: PackedStringArray = ["Content-Type: application/json"]
    var erro: int = http.request(url, headers, metodo, corpo)
    if erro != OK:
        push_error("Falha ao iniciar requisição para %s" % url)
        return {}

    var resposta: Array = await http.request_completed
    var result: int = resposta[0]
    var response_code: int = resposta[1]
    var body: PackedByteArray = resposta[3]

    if result != HTTPRequest.RESULT_SUCCESS:
        push_error("Erro de rede em %s. Result: %d" % [url, result])
        return {}
    if response_code < 200 or response_code >= 300:
        push_error("Status HTTP %d em %s" % [response_code, url])
        return {}

    var dados: Variant = JSON.parse_string(body.get_string_from_utf8())
    if dados == null:
        push_error("Resposta de %s não é JSON válido" % url)
        return {}
    return dados as Dictionary

Rodou, apareceu o IP de origem e o eco do POST no console, está funcionando. A partir daqui, trocar o httpbin pela sua API é trocar URL e campos.

Boas práticas antes de colocar num jogo de verdade

HTTPS já funciona. URLs https:// são validadas com o bundle de certificados que acompanha a Godot, sem configuração. Use sempre HTTPS em produção; HTTP puro expõe tudo que o jogador envia.

Defina timeout. A propriedade timeout do nó (em segundos) aborta requisições penduradas. O padrão é 0, sem limite, o que significa que um servidor mudo pode deixar seu jogo esperando para sempre. Um valor entre 5 e 15 segundos cobre a maioria dos casos; quando estoura, o result chega como um código de timeout, diferente de RESULT_SUCCESS, e cai no seu tratamento de erro normal.

Nunca bloqueie o jogo. A requisição já é assíncrona; não desfaça essa vantagem prendendo o jogador. Mostre um indicador de "carregando" (a variável carregando do exemplo existe para a UI ler), deixe o menu navegável e nunca condicione o frame à resposta.

Trate servidor fora do ar como caso normal. Vai acontecer. O jogo precisa continuar jogável: ranking indisponível vira uma mensagem "tente novamente", save na nuvem cai para o save local, notícia da tela inicial simplesmente não aparece. Erro de rede silenciosamente engolido é ruim, mas erro de rede que trava o jogo é pior.

Valide tudo que chega. Cheque result, cheque response_code, cheque o null do parse, use get() com padrão. Quatro checagens baratas que separam um jogo estável de um crash report.

Com esse alicerce pronto, os próximos passos são aplicações diretas do mesmo padrão: montar um ranking online na Godot é um POST de pontuação e um GET de lista, e implementar cloud save na Godot é subir e baixar um JSON de save. Os dois tutoriais assumem exatamente o que você acabou de aprender aqui: um nó, um sinal, duas checagens e JSON nos dois sentidos.

Perguntas frequentes

Preciso de plugin para fazer requisições HTTP na Godot 4?

Não. A Godot 4 traz o nó HTTPRequest de fábrica, sem plugin nem dependência externa. Ele cobre GET, POST e os demais métodos HTTP, envia headers customizados, funciona com HTTPS por padrão e entrega a resposta pelo sinal request_completed. Plugins só entram em cena para SDKs específicos (Steam, serviços de plataforma), não para HTTP puro.

Por que recebo o erro "HTTPRequest is processing a request"?

Porque cada nó HTTPRequest processa uma requisição por vez. Se você chama request() de novo antes de o sinal request_completed disparar, a Godot recusa a segunda chamada e imprime esse erro. As soluções são: esperar a resposta com await antes de disparar a próxima, criar um nó HTTPRequest para cada requisição simultânea, ou montar uma fila que despacha uma requisição por vez.

HTTPRequest funciona com HTTPS?

Sim, por padrão e sem configuração extra. A Godot valida o certificado do servidor usando o bundle de autoridades certificadoras que acompanha a engine, então uma URL https:// simplesmente funciona. Se o certificado for inválido ou autoassinado, a requisição falha com um result diferente de RESULT_SUCCESS, o que é o comportamento correto para proteger o jogador.

Qual a diferença entre result e response_code no request_completed?

O result diz se a requisição aconteceu do ponto de vista da rede: RESULT_SUCCESS significa que houve conexão e veio resposta; outros valores indicam DNS que não resolveu, conexão recusada ou timeout. O response_code é o status HTTP que o servidor devolveu: 200 é sucesso, 404 é recurso inexistente, 500 é erro do servidor. Cheque os dois, nessa ordem, porque um 404 chega com result de sucesso.

Posso usar await com HTTPRequest em vez de conectar o sinal?

Pode, e para fluxos lineares é o padrão mais limpo. Depois de chamar request(), escreva var resposta: Array = await http.request_completed e a função pausa sem travar o jogo até a resposta chegar. O Array traz os mesmos quatro valores do callback: result, response_code, headers e body. Para requisições recorrentes ou vindas de vários pontos do código, o sinal conectado continua sendo a melhor escolha.