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:
- Reads the
protocol
,host
, andport
from the fluent call or configuration. - Creates a
Tcp
object, enabling TLS ifhttps
is selected. - Applies the provided root CA certificate, if given.
- Connects to the server, performs the TLS handshake, and sends the request.
- 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()
= 0ok()
= falsegetErrorMessage()
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 toPico-Framework/1.0
Content-Type
: inferred from body or explicitly setAccept
: 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()
returnstrue
for 2xx codesresponse.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.