Skip to content

HTTP Client Guide

Introduction and Overview

The HTTP client provided by Pico-Framework is a lightweight yet powerful component designed for embedded systems that need to interact with external APIs, configuration services, or content delivery endpoints. Built on top of the same abstractions that power the framework's HTTP server, the client offers seamless support for both plain HTTP and encrypted HTTPS communication. It is engineered to provide modern web interoperability while remaining efficient in constrained environments.

Unlike large networking libraries designed for desktops or high-level platforms, the Pico-Framework HTTP client makes no assumptions about memory availability, operating system facilities, or exception handling. It works equally well on bare-metal RP2040 boards and FreeRTOS-based systems. TLS support is fully integrated through mbedTLS, and chunked transfer decoding is handled automatically without requiring the application developer to deal with low-level HTTP quirks.

The client is composed of three core classes:

  • HttpRequest: used to configure the method, target, headers, and body.
  • HttpResponse: stores the status, headers, and response body, or optionally streams it to a file.
  • HttpClient: performs the connection, sends the request, and populates the response.

This division allows fine control over all aspects of the HTTP lifecycle while keeping the interface compact and declarative.


Capabilities at a Glance

  • Fluent API for request construction using a chainable interface
  • Full TLS support with root CA verification via the Tcp abstraction
  • Chunked transfer decoding and gzip content support
  • Flexible response handling, including in-memory buffers or streamed-to-disk mode
  • Header manipulation for both request and response
  • Optional integration with JSON helpers for simplified API consumption
  • Configurable timeouts and ports for flexible transport scenarios

Basic Flow: Performing a Request

In a typical scenario, you instantiate a HttpClient, configure a HttpRequest, and provide a HttpResponse to capture the result:

HttpClient client;
HttpRequest request;
HttpResponse response;

request.setMethod("GET");
request.setHost("api.example.com");
request.setPath("/v1/device");
request.setProtocol("https");
request.setPort(443);

client.send(request, response);

After the request completes, the response object contains:

  • The HTTP status code (response.statusCode())
  • Any headers returned by the server
  • The response body, either as a string or file

This structure makes it easy to build one-off requests, recurring data fetchers, or fully-featured REST clients within your embedded application.


Example: Fetching Weather Data to File

HttpRequest req;
HttpResponse res;

req.setMethod("GET");
req.setProtocol("https");
req.setHost("api.open-meteo.com");
req.setPath("/v1/forecast?latitude=37.8&longitude=-122.4&hourly=temperature_2m");
req.setPort(443);

res.toFile("/data/weather.json");

HttpClient client;
client.send(req, res);

This code fetches a weather forecast and stores it directly to disk, avoiding memory pressure. Chunked transfer encoding from the server is handled automatically.


Example: Posting Sensor Data as JSON

HttpRequest req;
HttpResponse res;

req.setMethod("POST");
req.setProtocol("https");
req.setHost("api.example.com");
req.setPath("/v1/sensors");
req.setPort(443);
req.setJson({
    { "device_id", "pico-42" },
    { "temperature", 24.3 },
    { "humidity", 55.2 }
});

HttpClient client;
client.send(req, res);

if (res.statusCode() != 200) {
    logger.log("Post failed: " + res.errorMessage());
}

This demonstrates sending structured JSON data to a backend API using a secure connection, with minimal code.


Streaming Large Responses

To handle large payloads such as firmware binaries, log archives, or large JSON arrays, the response can be streamed directly to a file using:

res.toFile("/data/firmware.bin");

This ensures that RAM usage is minimized, and the framework avoids buffering content in memory. If toFile() is not set, the full body is buffered internally, which is fine for smaller JSON or text responses.


Summary

The Pico-Framework HTTP client brings a pragmatic and production-grade approach to HTTP communications in embedded systems. It supports everything you expect from a modern client — including TLS, headers, timeouts, chunked responses, and large file handling — but wraps it in a simple and consistent interface.

With support for fetching structured data, posting sensor telemetry, and downloading large resources, it empowers embedded applications to participate fully in modern web architectures — from configuration portals to IoT dashboards.

2. TLS and Connection Handling

Secure communication is critical for many embedded applications, especially when connecting to cloud services, APIs, or remote update servers. The Pico-Framework HTTP client includes first-class support for TLS via its internal Tcp abstraction, which seamlessly integrates mbedTLS for encrypted connections.

TLS ensures that data sent between your device and the server cannot be intercepted or tampered with. It also allows the device to verify the identity of the server using a root certificate authority (CA), preventing man-in-the-middle attacks.

All of this is performed behind the scenes by the client — but the application retains control over configuration and error handling.


Connection Lifecycle

Each time you perform a request, the framework:

  1. Reads the protocol, host, and port from the fluent call or configuration.
  2. Creates a Tcp object, enabling TLS if https is selected.
  3. Applies the provided root CA certificate, if given.
  4. Connects to the server, performs the TLS handshake, and sends the request.
  5. Parses the response and returns it to the caller.

No persistent connections are used — each request is independent, ensuring clarity and memory safety.


TLS Configuration

TLS is enabled automatically when the protocol is set to "https" — either via setProtocol() or as part of the URL passed to get(), post(), etc. A valid PEM-encoded root certificate must be provided via:

request.setRootCACertificate(BROWSER_ROOT_CA);

This certificate must remain valid and in scope during the request lifecycle. Verification is performed by mbedTLS inside the Tcp wrapper.


Real Example: TLS Request to Raspberry Pi Endpoint

The following example demonstrates a successful TLS request using the fluent interface to fetch a .sig file from the Raspberry Pi OS net installer:

HttpResponse response;
HttpRequest request;

printf("===========================\n");
printf("\nTLS test to Raspberry Pi example\n");

response = request
    .setRootCACertificate(BROWSER_ROOT_CA)
    .get("https://fw-download-alias1.raspberrypi.com/net_install/boot.sig");

if (!response.ok()) {
    printf("RPi Failed to fetch data\n");
} else {
    printf("TLS request sent successfully\n");
    printf("RPi Response: %s\n", response.getBody().c_str());
}

printf("===========================\n");

This is the simplest and most reliable way to perform a TLS GET using the fluent interface. The framework automatically configures the underlying TCP and TLS stack, and validates the remote server using the provided certificate.


Internal TLS Handling

The fluent interface wraps lower-level socket setup:

  • Enables TLS if https is detected
  • Applies the root certificate to the TLS context
  • Performs connection, handshake, and response processing

All of this is abstracted behind HttpRequest::get(), post(), send(), and their certificate-aware variants.


Time and Certificate Validation

TLS depends on accurate system time for certificate validation. If your system does not sync via NTP or initialize an RTC before requests are made, certificates may appear invalid or expired.

Make sure your system time is set before performing secure requests.


Error Handling

If the TLS connection fails (due to time, certificate, DNS, or socket issues), the HttpResponse will contain:

  • statusCode() = 0
  • ok() = false
  • getErrorMessage() describing the problem

Example:

if (!response.ok()) {
    logger.log("Connection failed: " + response.getErrorMessage());
}

This allows clean differentiation between transport errors and HTTP status codes (e.g., 404 or 500).


Plain HTTP (Non-TLS)

For internal or testing endpoints, http may be used instead of https. TLS will not be activated and the socket remains unencrypted. Example:

response = request.get("http://example.local/status");

This mode is useful during local development or where encryption is unnecessary.


Summary

TLS support in Pico-Framework is fully integrated and secure:

  • Use .get("https://...") or .setProtocol("https")
  • Call .setRootCACertificate(...) with a valid PEM CA
  • Rely on the framework to handle validation and handshake
  • Check .ok() and .getErrorMessage() on failure

With these features, the framework provides a robust path to secure communication for telemetry, firmware updates, and third-party APIs in embedded systems.

3. Request Construction and Body Handling

Pico-Framework provides a highly expressive and fluent interface for constructing HTTP requests, allowing embedded developers to configure method, headers, body, and encoding with minimal code and maximum readability. This section covers:

  • Fluent method calls: get(), post(), put(), delete()
  • Setting headers, including Content-Type and User-Agent
  • Sending raw body strings or JSON objects
  • Loading body data from a file (e.g., for firmware uploads or large payloads)
  • Setting timeouts and connection parameters

Fluent Method Interface

Requests can be created fluently by chaining methods directly on a HttpRequest object. This pattern increases readability and reduces boilerplate.

HttpRequest request;
HttpResponse response;

response = request
    .setMethod("POST")
    .setProtocol("https")
    .setHost("api.example.com")
    .setPort(443)
    .setPath("/v1/status")
    .setHeader("X-Custom", "value")
    .setBody("raw body data")
    .send();

For convenience, shorthand methods are also provided for GET, POST, PUT, and DELETE:

response = request
    .setHeader("X-Test", "123")
    .post("https://api.example.com/v1/status", "payload here");

These implicitly configure the method, protocol, host, port, and path by parsing the URL string. They're ideal for simple one-off requests.


Setting Headers

Custom headers are set via:

request.setHeader("X-Token", "abc123");

You can override standard headers such as:

  • User-Agent: defaults to Pico-Framework/1.0
  • Content-Type: inferred from body or explicitly set
  • Accept: optional

Example:

request
    .setHeader("Content-Type", "application/json")
    .setHeader("Accept", "application/json");

All headers are stored internally and transmitted with the request in standard HTTP format.


JSON Body Support

You can send JSON bodies using either raw strings or the structured helper:

request.setJson({
    { "device", "pico-01" },
    { "uptime", 3600 },
    { "ok", true }
});

This automatically sets the body and the correct Content-Type header. It's a preferred option when talking to APIs or cloud services.

Example using the fluent post helper:

HttpResponse res = request
    .setRootCACertificate(CLOUD_CA)
    .post("https://api.example.com/v1/device", {
        { "status", "active" },
        { "temperature", 24.8 }
    });

Body from File

For large requests, you can load the body from a file (e.g., firmware image, binary blob):

request.setBodyFromFile("/firmware/update.bin");

This streams the file content during transmission and avoids loading the full body into RAM. The framework will set Content-Length automatically based on the file size.

This feature is especially useful when POSTing or PUTing large payloads to an update server.


Sending Raw String Body

You can set any raw string body:

request.setBody("raw POST body here");

Use this when constructing custom payloads such as form-encoded strings or text commands. Combine with setHeader("Content-Type", "...") as needed.


Timeout and Port Control

By default, ports are inferred from the URL (80 for http, 443 for https). You may override this via:

request.setPort(8080);

Timeouts can be adjusted if the server is slow to respond:

request.setTimeoutMs(10000);  // 10 second timeout

This ensures responsiveness and allows for retry logic if needed.


Summary

Request construction in Pico-Framework is both declarative and flexible:

  • Chainable interface supports common HTTP methods
  • Headers and bodies are easy to configure
  • JSON and file bodies are supported out-of-the-box
  • Timeouts and ports are customizable
  • Supports both simple GETs and rich multipart POSTs

This gives developers a complete toolkit for interacting with modern web APIs in just a few lines of embedded C++.

4. Response Handling and Streaming

Pico-Framework provides a flexible, memory-conscious way to handle HTTP responses — whether small JSON payloads or large streamed files. The HttpResponse object gives your application direct access to status codes, headers, body content, and error reporting. It also includes special support for large response handling via streaming to file.

This section covers:

  • Accessing status code and headers
  • Reading response body (in memory)
  • Saving large responses to a file
  • Automatic handling of chunked transfer encoding
  • Error detection and logging

Checking Response Status

Every HTTP response includes a status code:

if (response.ok()) {
    printf("Success! Code = %d\n", response.statusCode());
} else {
    printf("Error: %s\n", response.getErrorMessage().c_str());
}
  • response.ok() returns true for 2xx codes
  • response.statusCode() gives the exact code (e.g., 200, 404, 503)
  • response.getErrorMessage() provides human-readable failure details

These checks are useful for conditional logic, retries, or diagnostics.


Accessing Headers

All headers sent by the server are stored and can be queried:

std::string contentType = response.getHeader("Content-Type");
std::string encoding = response.getHeader("Content-Encoding");

Use this to inspect server behavior or handle special content cases.


Reading the Body In-Memory

For most responses (e.g., JSON or text payloads under 1 KB), the body is returned as a standard string:

std::string content = response.getBody();
printf("Body: %s\n", content.c_str());

This is suitable for diagnostics, status queries, and lightweight data processing.

Internally, the framework reads the response and buffers it in memory — up to a configurable size limit.


Streaming Large Responses to File

If the response body is large (e.g., a firmware binary or image), you can direct the body to a file using:

request.toFile("/download/fw.bin");

This tells the framework to stream the incoming response directly to disk, avoiding RAM exhaustion. The file is created before headers are received, and the body is written as it arrives — line by line or chunk by chunk.

Example:

HttpResponse response = request
    .toFile("/data/latest.jpg")
    .get("https://cdn.example.com/image.jpg");

if (response.ok()) {
    logger.log("Image saved to flash.");
} else {
    logger.log("Download failed: " + response.getErrorMessage());
}

This feature is ideal for OTA downloads, remote resource fetching, and logging.


Automatic Chunked Transfer Support

Many modern APIs and web servers use Transfer-Encoding: chunked to send responses where the total length is unknown up front.

The framework automatically handles chunked transfer decoding:

  • Reads each chunk size
  • Appends content correctly
  • Stops on a zero-length final chunk

You do not need to write any special logic — chunked responses are handled transparently whether streamed or buffered.


Error and Timeout Handling

If the response fails (DNS error, TLS failure, 404, etc.), the HttpResponse will indicate failure:

if (!response.ok()) {
    std::string reason = response.getErrorMessage();
    int code = response.statusCode();  // May be 0 if connection failed
    logger.log("Request failed: " + reason);
}

This makes it easy to separate networking issues from server-side HTTP failures.


Summary

The HttpResponse object gives applications fine-grained control over how HTTP replies are processed:

  • Full status and header access
  • In-memory body reads for small responses
  • Safe, efficient file-based streaming for large downloads
  • Transparent chunked transfer decoding
  • Unified error handling across plain and TLS connections

Combined with fluent request construction, this makes Pico-Framework a robust platform for embedded devices that consume cloud services or pull remote resources.

5. Examples and Use Cases

This section brings together the previous concepts through practical, real-world examples. It demonstrates common patterns like API calls, file downloads, device registration, and diagnostics — showcasing the expressiveness and utility of the Pico-Framework HTTP client in embedded workflows.

Each example builds on the fluent interface and demonstrates best practices in memory use, TLS handling, and error management.


Example 1: Device Registration via JSON POST

Register a device with a cloud backend using JSON payload:

HttpRequest request;
HttpResponse response = request
    .setRootCACertificate(CLOUD_CA)
    .setHeader("Content-Type", "application/json")
    .post("https://api.example.com/devices/register", {
        { "id", "pico-01" },
        { "firmware", "1.0.3" },
        { "uptime", 4321 }
    });

if (!response.ok()) {
    logger.log("Registration failed: " + response.getErrorMessage());
}

This demonstrates a typical cloud API integration using TLS, JSON, and the fluent post() call.


Example 2: GET with Query Parameters

Fetch configuration from a remote server using query string values:

HttpRequest request;
request.setProtocol("https")
       .setHost("api.example.com")
       .setPath("/config")
       .setQueryParam("device", "pico-01")
       .setQueryParam("version", "1.0");

HttpResponse response = request.get();

if (response.ok()) {
    std::string config = response.getBody();
    parseAndApplyConfig(config);
}

This style avoids body payloads and is ideal for simple lookups or control commands.


Example 3: Stream Large File to Flash

Download a firmware image from a secure host and save directly to flash:

HttpRequest request;
HttpResponse response = request
    .setRootCACertificate(FIRMWARE_CA)
    .toFile("/firmware/latest.bin")
    .get("https://fw.example.com/updates/latest.bin");

if (response.ok()) {
    logger.log("Firmware downloaded to /firmware/latest.bin");
} else {
    logger.log("Firmware download failed: " + response.getErrorMessage());
}

This ensures that even large payloads do not exceed RAM limits.


Example 4: TLS Fetch with Error Checking

Request a signature file using TLS and handle failure gracefully:

HttpRequest request;
HttpResponse response = request
    .setRootCACertificate(BROWSER_ROOT_CA)
    .get("https://fw-download-alias1.raspberrypi.com/net_install/boot.sig");

if (response.ok()) {
    logger.log("Signature file received");
} else {
    logger.log("TLS fetch failed: " + response.getErrorMessage());
}

This real-world example is based on working tests and shows how easily secure downloads can be added.


Example 5: Use with Local HTTP Service

Communicate with a local web service running on the LAN or on the same device:

HttpRequest request;
HttpResponse response = request.get("http://192.168.4.1/status");

if (response.ok()) {
    std::string json = response.getBody();
    updateStatusFromJson(json);
}

This approach is ideal during setup, testing, or device-to-device communication.


Example 6: Timeout and Retry

Add timeout logic and retry on failure:

HttpRequest request;
request.setTimeoutMs(5000);  // 5 seconds
int attempts = 0;
HttpResponse response;

do {
    response = request.get("http://api.local/device/status");
    attempts++;
} while (!response.ok() && attempts < 3);

if (!response.ok()) {
    logger.log("Status fetch failed after retries");
}

This pattern increases robustness in noisy or unstable networks.


Summary

These examples show how Pico-Framework's HTTP client can be used for:

  • Secure device registration
  • Downloading firmware or media
  • Communicating with cloud or LAN services
  • Supporting streaming, chunked, and large responses
  • Managing retries, timeouts, and TLS

By combining a fluent interface with secure, memory-aware design, it enables a broad range of network interactions even on resource-constrained embedded devices.