Description
Changelog
Reviews (1)

C3 HTTP Request (C3Http)

C3Http is a lightweight nodeless replacement for HTTPRequest. It offers significant improvements in ergonomics, performance, and testability.

Full documentation

Here is a complete working example of how to use C3Http in a script:

extends Node2D

func _ready() -> void:
    var res := await C3Http.request("https://jsonplaceholder.typicode.com/todos/1")
    if res.ok:
        print(res.body.get_string_from_utf8())  # .body is PackedStringArray
        print(res.text)                         # .text is String
        print(res.json["title"])                # .json is Variant
    else:
        print(res.error)         # "[http] status=404 Request failed with status 404."
        print(res.error.status)  # 404

Features

  • Static await-able request() callable from any script — no Node to add or configure
  • Every call returns a typed Response object — a single if not res.ok check covers transport failures, timeouts, and non-2xx statuses alike
  • Per-request Options that mirror HTTPRequest's properties (use_threads, accept_gzip, etc.), plus additional features
  • HTTP keep-alive — set Options.session to pool and reuse connections across calls to the same host
  • Server-Sent Events (SSE) — pass an on_sse_event callback to consume a streaming text/event-stream response incrementally, with the Last-Event-ID cursor and retry: backoff surfaced for reconnects
  • Download progress — pass an on_progress callback to track (bytes_received, total_bytes) as the body arrives
  • Cancellation token — cancel an in-flight request from another coroutine or signal handler
  • Connection status — pass an on_status_changed callback to observe the HTTPClient lifecycle (resolving, connecting, requesting, body)
  • Built-in test mock — C3Http.Mock intercepts all requests in tests without a network, with stubs to configure responses and a call log for assertions

Comparison with HTTPRequest

Feature C3Http HTTPRequest
No Node to add or configure
await-able (no signal wiring)
Single ok check (transport + non-2xx)
Decoded text body accessor
Parsed json body accessor
Server-Sent Events (SSE) streaming
Typed RequestError with Kind
Built-in test mock
HTTP keep-alive and connection reuse
Cancellation
Timeout
Gzip decompression
Redirect following
Download to file
Body size limit
Custom TLS options
Raw request body (bytes)
HTTP/HTTPS proxy
Download progress events
Connection status checking
Threaded requests (off main loop)

Benchmarks

A benchmark analysis was performed using Godot 4.7 on Windows 11 against a remote API. The benchmark scene can be found in examples/benchmark/. The raw results can be found in BENCHMARK.md, and docs/benchmarks.md has the full analysis.

Highlights:

  • C3Http excels at large downloads: In its default use_threads = false mode, HTTPRequest reads exactly one chunk per frame, capping throughput at download_chunk_size × frame_rate (~3.93 MB/s at 60 fps with 64 KB chunks) no matter how fast the link is. C3Http drains every available chunk each frame, so its time tracks bandwidth instead — ~5× faster at 8 MB (487 ms vs 2367 ms) and ~9× faster at 32 MB (984 ms vs 8800 ms).
  • Sessions (keep-alive) are the strongest lever: With C3Http, passing a shared Session via Options.session reuses a warm TLS/TCP connection, eliminating ~125 ms of handshake and TCP slow-start per request: single-request latency drops from ~162 ms to ~34 ms, and a 400 KB download from ~300 ms to ~51 ms. Native HTTPRequest has no equivalent.
  • Latency: C3Http is ~1 frame faster when use_threads = false: With use_threads = false (the default for both clients) at any capped frame rate, C3Http resolves one frame earlier than native (e.g. 183 ms vs 200 ms at 60 fps) because when the underlying HTTPClient's status transitions (e.g. from requesting to reading the body after headers arrive), C3Http re-reads the client status in the same loop iteration rather than waiting for the next frame.

Compatibility

Tested on Godot 4.7.x with automated (GUT) and manual tests. Manually verified to work back to Godot 4.2.0.

Installation

Click the "Asset Store" tab at the top of the Godot editor and search for "C3 HTTP Request". Then click "Download" and "Install". The addon will be automatically added to your project, and C3Http will be available as a global class immediately — no plugin activation required.

Alternatively, download the latest release from GitHub and copy the addons/c3_http_request folder into your project's addons/ directory.

Quick start

Install the addon (see Installation above) and C3Http is available immediately as a global class. Await its static request() method to make a request and get a typed Response object back:

# GET
var res := await C3Http.request("https://api.example.com/todos/1")
if not res.ok:
    push_error(str(res.error))
    return
print(res.status)  # 200
print(res.body)    # raw response body bytes (PackedByteArray)
print(res.text)    # response body decoded as UTF-8
print(res.json)    # response body parsed as JSON (Variant; null if invalid)

# POST with a JSON body and custom headers
var res2 := await C3Http.request(
    "https://api.example.com/posts",
    PackedStringArray([
        "Content-Type: application/json",
        "Authorization: Bearer " + token,
    ]),
    HTTPClient.METHOD_POST,
    '{"title": "hello"}'
)

# POST a raw binary body (sent as-is, not UTF-8 encoded)
var res3 := await C3Http.request_raw(
    "https://api.example.com/upload",
    PackedStringArray(["Content-Type: application/octet-stream"]),
    HTTPClient.METHOD_POST,
    payload  # a PackedByteArray
)

# Per-request options
var opts := C3Http.Options.new()
opts.timeout = 10.0
var res4 := await C3Http.request(url, PackedStringArray(), HTTPClient.METHOD_GET, "", opts)

Response

Every call returns a Response, whether it succeeded or not. ok is the field you'll check most: true on a 2xx status, false for anything else (transport failure, timeout, or a non-2xx status). Beyond that, status and headers mirror what the server sent, and the body is available three ways depending on what you need: body as raw bytes, text decoded as UTF-8, or json parsed as a Varianttext and json are computed lazily on first access and cached, so reading them repeatedly is cheap.

var res := await C3Http.request("https://api.example.com/todos/1")
if not res.ok:
    push_error(str(res.error))  # RequestError; See "Error handling" section.
    return
print(res.status)        # 200
print(res.headers)       # PackedStringArray of "Name: Value" strings
print(res.body)          # raw response body bytes (PackedByteArray)
print(res.text)          # response body decoded as UTF-8
print(res.json)          # response body parsed as JSON (Variant; null if invalid)

See the Response reference for more information.

Options

The optional last argument to request() lets you tune a single call without changing its shape. Most fields have sensible defaults and only need setting when you want non-default behavior — a timeout, a body size cap, a proxy, TLS overrides for self-signed certificates, or a background thread via use_threads (see Threaded requests). The more involved features — cancellation, SSE, download progress, connection status, and session reuse — each get their own section below with a working example.

var opts := C3Http.Options.new()
opts.timeout = 10.0
opts.max_redirects = 0
var res := await C3Http.request(url, PackedStringArray(), HTTPClient.METHOD_GET, "", opts)

See the Options reference for more information.

Error handling

When res.ok is false, res.error is a RequestError describing what went wrong, so you can react differently to a timeout than to a 404 or a cancellation. Its kind field (a RequestError.Kind enum: TRANSPORT, HTTP, CLIENT, TIMEOUT, CANCELLED, BODY_SIZE_LIMIT_EXCEEDED) categorizes the failure, and str(error) gives a compact one-line summary for logging:

if not res.ok:
    if res.error.kind == C3Http.RequestError.Kind.TIMEOUT:
        print("timed out, retrying...")
    else:
        push_error(str(res.error))  # e.g. "[http] status=404 Request failed with status 404."
    return

See the RequestError reference for more information.

Cancellation

Pass a CancellationToken via Options.cancellation_token and call token.cancel() from anywhere to abandon an in-flight request. The polling loop checks the token between iterations and returns a Response with error.kind == CANCELLED.

var token := C3Http.CancellationToken.new()
var opts := C3Http.Options.new()
opts.cancellation_token = token

var request := C3Http.request(
    "https://api.example.com/slow",
    PackedStringArray(),
    HTTPClient.METHOD_GET,
    "",
    opts
)
get_tree().create_timer(2.0).timeout.connect(token.cancel)

var res := await request
if not res.ok and res.error.kind == C3Http.RequestError.Kind.CANCELLED:
    print("request cancelled")

See the Cancellation guide and C3Http.CancellationToken reference for usage and examples.

Server-Sent Events (SSE)

Set Options.on_sse_event to a Callable to consume a streaming text/event-stream response. The callback fires once per event — on_sse_event.call(data, event_type, last_event_id) — as events arrive, and the await resolves to a final Response once the stream closes. Response.sse_retry_ms surfaces the server's retry: backoff for reconnects.

var opts := C3Http.Options.new()
opts.on_sse_event = func(data: String, event_type: String, last_event_id: String) -> void:
    print("[%s] %s" % [event_type, data])

var res := await C3Http.request(
    "https://api.example.com/stream",
    PackedStringArray(),
    HTTPClient.METHOD_GET,
    "",
    opts
)

See the SSE guide for full details, including reconnect patterns.

Download progress

Set Options.on_progress to a Callable to track a download as it arrives. The callback fires once per chunk — on_progress.call(bytes_received, total_bytes) — where total_bytes is the Content-Length or -1 when the server doesn't send one.

var opts := C3Http.Options.new()
opts.on_progress = func(bytes_received: int, total_bytes: int) -> void:
    print("%d / %d" % [bytes_received, total_bytes])

var res := await C3Http.request(
    "https://api.example.com/large-file",
    PackedStringArray(),
    HTTPClient.METHOD_GET,
    "",
    opts
)

See the Download progress guide for usage and examples.

Connection status

Set Options.on_status_changed to a Callable to observe the underlying HTTPClient as it advances through its lifecycle — the equivalent of HTTPRequest's get_http_client_status(). The callback fires once per change with an HTTPClient.Status value.

var opts := C3Http.Options.new()
opts.on_status_changed = func(status: HTTPClient.Status) -> void:
    print(status)

var res := await C3Http.request(
    "https://api.example.com/todos/1",
    PackedStringArray(),
    HTTPClient.METHOD_GET,
    "",
    opts
)

See the Connection status guide for usage and examples.

Threaded requests

By default the polling loop yields to the scene tree once per frame (the same cadence as HTTPRequest). Set Options.use_threads to true to run the loop on a dedicated background thread that polls at OS speed — lowering latency for fast endpoints and keeping the main thread free during large or streaming downloads. The await API is unchanged; callbacks are auto-marshaled back to the main thread.

var opts := C3Http.Options.new()
opts.use_threads = true
var res := await C3Http.request(
    "https://api.example.com/todos/1",
    PackedStringArray(),
    HTTPClient.METHOD_GET,
    "",
    opts
)

See the Threaded requests guide for details and caveats.

Sessions (Keep-Alive)

Set Options.session to a Session object to pool and reuse connections across calls to the same host, skipping the TCP/TLS handshake on subsequent requests. Session exposes max_connections_per_host (default 6) and idle_timeout (default 60.0 seconds). Call session.close() to release all pooled connections early.

var session := C3Http.Session.new()
var opts := C3Http.Options.new()
opts.session = session

for i in 5:
    var res := await C3Http.request(
        "https://api.example.com/todos/%d" % i,
        PackedStringArray(), HTTPClient.METHOD_GET,
        "",
        opts
    )

See the Sessions guide for usage and examples.

Testing

C3Http.Mock intercepts all request() calls in tests without touching the network. Install it in before_each and uninstall in after_each; register canned responses with mock.stub() and assert outgoing calls via mock.calls / mock.call_count / mock.last_call.

extends GutTest

var mock: C3Http.Mock

func before_each() -> void:
    mock = C3Http.Mock.new()
    mock.install()

func after_each() -> void:
    mock.uninstall()

func test_fetches_todo() -> void:
    mock.stub().ok({"title": "hello"})
    var res := await C3Http.request("https://api.example.com/todos/1")
    assert_eq(res.json["title"], "hello")
    assert_eq(mock.call_count, 1)

See the Testing guide and Mock reference for full usage, including stubbing and call assertions. For a real-world example, see the tests/test_public_api.gd file, which use the Mock class to verify the public API of C3Http without touching the network.

Changelog for version v0.4.0

No changelog provided for this version.

Reviews

Recommended by Cuppixx - 19 June 2026

Login to write a review.

Consider supporting the creators!

If you enjoyed this asset consider supporting its creator. Follow the link below.