With WiFi connectivity established, we can now make HTTP requests to fetch data from a web API. This part demonstrates DNS resolution and HTTP GET requests using Zephyr’s socket and HTTP client APIs.
| Target | ESP32-S3-DevKitC |
| Zephyr version | 4.3.0 |
| Level | Beginner |
Tutorial Series:
- WiFi Connectivity
- HTTP Client (this part)
- HTTPS with TLS
Prerequisites
This tutorial requires:
- Completed Part 1: WiFi Connectivity with a working WiFi connection
- Basic understanding of TCP/IP sockets and the HTTP protocol
Familiarity with these concepts is helpful:
- BSD Sockets - the standard socket API used here
- HTTP/1.1 Protocol - the application protocol for web requests
- DNS Resolution - how hostnames are translated to IP addresses
Configuration
Add the following to prj.conf to enable sockets, HTTP client, and DNS resolution:
# Sockets
CONFIG_NET_SOCKETS=y
CONFIG_POSIX_API=y
# HTTP Client
CONFIG_HTTP_CLIENT=y
# DNS Resolver
CONFIG_DNS_RESOLVER=y
CONFIG_DNS_SERVER_IP_ADDRESSES=y
CONFIG_DNS_SERVER1="8.8.8.8"
The CONFIG_POSIX_API option provides familiar BSD socket functions like getaddrinfo(), socket(), and connect().
Why 8.8.8.8? Unlike more complex operating systems, Zephyr does not automatically obtain DNS server addresses from DHCP. You must configure a DNS server explicitly. We use Google’s public DNS (8.8.8.8) as a reliable default that works on most networks. Alternatively, you could use Cloudflare’s 1.1.1.1 or your local network’s DNS server.
Also increase the stack and buffer sizes to accommodate HTTP operations:
# Stack sizes for ESP32 (increased for HTTP)
CONFIG_MAIN_STACK_SIZE=8192
# Network buffers (increased for HTTP)
CONFIG_NET_PKT_RX_COUNT=16
CONFIG_NET_PKT_TX_COUNT=16
CONFIG_NET_BUF_RX_COUNT=64
CONFIG_NET_BUF_TX_COUNT=64
The complete prj.conf after these additions:
# Networking
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_DHCPV4=y
CONFIG_NET_TCP=y
# WiFi
CONFIG_WIFI=y
CONFIG_NET_L2_WIFI_MGMT=y
CONFIG_NET_MGMT=y
CONFIG_NET_MGMT_EVENT=y
CONFIG_NET_MGMT_EVENT_INFO=y
# Sockets
CONFIG_NET_SOCKETS=y
CONFIG_POSIX_API=y
# HTTP Client
CONFIG_HTTP_CLIENT=y
# DNS Resolver
CONFIG_DNS_RESOLVER=y
CONFIG_DNS_SERVER_IP_ADDRESSES=y
CONFIG_DNS_SERVER1="8.8.8.8"
# Stack sizes for ESP32
CONFIG_MAIN_STACK_SIZE=8192
CONFIG_NET_TX_STACK_SIZE=2048
CONFIG_NET_RX_STACK_SIZE=2048
# Network buffers
CONFIG_NET_PKT_RX_COUNT=16
CONFIG_NET_PKT_TX_COUNT=16
CONFIG_NET_BUF_RX_COUNT=64
CONFIG_NET_BUF_TX_COUNT=64
# Logging
CONFIG_LOG=y
CONFIG_NET_LOG=y
# WiFi credentials (edit these)
CONFIG_WIFI_SSID="YourSSID"
CONFIG_WIFI_PSK="YourPassword"
HTTP GET Implementation
The HTTP client uses standard socket operations followed by Zephyr’s HTTP client helper. Update src/main.c with the HTTP request function.
Unlike Part 1 where we simply logged events and returned, here we need to wait for the IP address before making HTTP requests. We add a semaphore (ip_obtained_sem) that the IPv4 callback signals when DHCP completes, allowing main() to block until the network is ready.
We use JSONPlaceholder as our test endpoint - a free, public REST API designed for testing and prototyping. It requires no authentication, returns small JSON responses, and is reliably available. The /todos/1 endpoint returns a simple JSON object, perfect for verifying our HTTP client works:
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/net/wifi_mgmt.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_event.h>
#include <zephyr/net/socket.h>
#include <zephyr/net/http/client.h>
#include <zephyr/posix/netdb.h>
#include <zephyr/posix/unistd.h>
#include <zephyr/posix/sys/socket.h>
#include <errno.h>
LOG_MODULE_REGISTER(wifi_app, LOG_LEVEL_INF);
#define HTTP_HOST "jsonplaceholder.typicode.com"
#define HTTP_PORT "80"
#define HTTP_PATH "/todos/1"
static struct net_mgmt_event_callback wifi_cb;
static struct net_mgmt_event_callback ipv4_cb;
static K_SEM_DEFINE(ip_obtained_sem, 0, 1);
static uint8_t recv_buf[512];
static int response_cb(struct http_response *rsp,
enum http_final_call final_data,
void *user_data)
{
if (final_data == HTTP_DATA_MORE) {
LOG_INF("Partial data received (%zd bytes)", rsp->data_len);
} else if (final_data == HTTP_DATA_FINAL) {
LOG_INF("HTTP Status: %s", rsp->http_status);
LOG_INF("Response body (%zd bytes):", rsp->data_len);
LOG_INF("%.*s", (int)rsp->data_len, rsp->recv_buf);
}
return 0;
}
static int http_get_request(void)
{
struct addrinfo hints;
struct addrinfo *res;
int sock;
int ret;
LOG_INF("Resolving %s...", HTTP_HOST);
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
ret = getaddrinfo(HTTP_HOST, HTTP_PORT, &hints, &res);
if (ret != 0) {
LOG_ERR("DNS lookup failed: %d", ret);
return ret;
}
LOG_INF("DNS resolved, creating socket...");
sock = socket(res->ai_family, res->ai_socktype, IPPROTO_TCP);
if (sock < 0) {
LOG_ERR("Socket creation failed: %d", errno);
freeaddrinfo(res);
return -errno;
}
LOG_INF("Connecting to %s:%s...", HTTP_HOST, HTTP_PORT);
ret = connect(sock, res->ai_addr, res->ai_addrlen);
freeaddrinfo(res);
if (ret < 0) {
LOG_ERR("Connect failed: %d", errno);
close(sock);
return -errno;
}
LOG_INF("Connected, sending HTTP GET %s...", HTTP_PATH);
struct http_request req = {
.method = HTTP_GET,
.url = HTTP_PATH,
.host = HTTP_HOST,
.protocol = "HTTP/1.1",
.response = response_cb,
.recv_buf = recv_buf,
.recv_buf_len = sizeof(recv_buf),
};
ret = http_client_req(sock, &req, 5 * MSEC_PER_SEC, NULL);
if (ret < 0) {
LOG_ERR("HTTP request failed: %d", ret);
} else {
LOG_INF("HTTP request completed (%d bytes sent)", ret);
}
close(sock);
return ret;
}
/* WiFi event handlers from Part 1 */
static void wifi_event_handler(struct net_mgmt_event_callback *cb,
uint64_t mgmt_event, struct net_if *iface)
{
if (mgmt_event == NET_EVENT_WIFI_CONNECT_RESULT) {
LOG_INF("WiFi connected");
} else if (mgmt_event == NET_EVENT_WIFI_DISCONNECT_RESULT) {
LOG_INF("WiFi disconnected");
}
}
static void ipv4_event_handler(struct net_mgmt_event_callback *cb,
uint64_t mgmt_event, struct net_if *iface)
{
if (mgmt_event == NET_EVENT_IPV4_ADDR_ADD) {
struct net_if_ipv4 *ipv4 = iface->config.ip.ipv4;
if (ipv4) {
char addr_str[NET_IPV4_ADDR_LEN];
for (int i = 0; i < NET_IF_MAX_IPV4_ADDR; i++) {
if (ipv4->unicast[i].ipv4.is_used) {
net_addr_ntop(AF_INET,
&ipv4->unicast[i].ipv4.address.in_addr,
addr_str, sizeof(addr_str));
LOG_INF("IP Address: %s", addr_str);
}
}
}
k_sem_give(&ip_obtained_sem);
}
}
static int connect_wifi(void)
{
struct net_if *iface = net_if_get_default();
struct wifi_connect_req_params params = {
.ssid = CONFIG_WIFI_SSID,
.ssid_length = strlen(CONFIG_WIFI_SSID),
.psk = CONFIG_WIFI_PSK,
.psk_length = strlen(CONFIG_WIFI_PSK),
.channel = WIFI_CHANNEL_ANY,
.band = WIFI_FREQ_BAND_2_4_GHZ,
.security = WIFI_SECURITY_TYPE_PSK,
};
LOG_INF("Connecting to %s...", CONFIG_WIFI_SSID);
return net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, ¶ms, sizeof(params));
}
int main(void)
{
int ret;
LOG_INF("ESP32-S3 WiFi + HTTP Client Example");
/* Register WiFi event callback */
net_mgmt_init_event_callback(&wifi_cb, wifi_event_handler,
NET_EVENT_WIFI_CONNECT_RESULT |
NET_EVENT_WIFI_DISCONNECT_RESULT);
net_mgmt_add_event_callback(&wifi_cb);
/* Register IPv4 event callback */
net_mgmt_init_event_callback(&ipv4_cb, ipv4_event_handler,
NET_EVENT_IPV4_ADDR_ADD);
net_mgmt_add_event_callback(&ipv4_cb);
/* Wait for WiFi to initialize */
k_sleep(K_SECONDS(2));
/* Connect to WiFi */
ret = connect_wifi();
if (ret) {
LOG_ERR("WiFi connect request failed: %d", ret);
return ret;
}
/* Wait for IP address */
LOG_INF("Waiting for IP address...");
ret = k_sem_take(&ip_obtained_sem, K_SECONDS(30));
if (ret < 0) {
LOG_ERR("Timeout waiting for IP address");
return ret;
}
/* Small delay to ensure network stack is ready */
k_sleep(K_MSEC(500));
/* Make HTTP request */
ret = http_get_request();
if (ret < 0) {
LOG_ERR("HTTP GET failed: %d", ret);
}
return 0;
}
How It Works
The http_get_request() function follows a standard socket workflow:
-
DNS Resolution:
getaddrinfo()resolves the hostname to an IP address using the configured DNS server. -
Socket Creation:
socket()creates a TCP socket withIPPROTO_TCP. -
Connection:
connect()establishes the TCP connection to the server. -
HTTP Request: Zephyr’s
http_client_req()helper sends a properly formatted HTTP request and handles the response via callback. -
Cleanup:
close()terminates the connection.
The response callback response_cb() receives data as it arrives. For small responses, HTTP_DATA_FINAL indicates the complete response is available. Larger responses may arrive in chunks with HTTP_DATA_MORE.
Building and Flashing
west build -b esp32s3_devkitc/esp32s3/procpu
west flash
west espressif monitor
Expected Output
[00:00:00.000,000] <inf> wifi_app: ESP32-S3 WiFi + HTTP Client Example
[00:00:02.000,000] <inf> wifi_app: Connecting to YourSSID...
[00:00:02.000,000] <inf> wifi_app: Waiting for IP address...
[00:00:05.xxx,xxx] <inf> wifi_app: WiFi connected
[00:00:06.xxx,xxx] <inf> wifi_app: IP Address: 192.168.x.x
[00:00:06.xxx,xxx] <inf> wifi_app: Resolving jsonplaceholder.typicode.com...
[00:00:06.xxx,xxx] <inf> wifi_app: DNS resolved, creating socket...
[00:00:06.xxx,xxx] <inf> wifi_app: Connecting to jsonplaceholder.typicode.com:80...
[00:00:06.xxx,xxx] <inf> wifi_app: Connected, sending HTTP GET /todos/1...
[00:00:06.xxx,xxx] <inf> wifi_app: HTTP Status: 200 OK
[00:00:06.xxx,xxx] <inf> wifi_app: Response body (83 bytes):
[00:00:06.xxx,xxx] <inf> wifi_app: {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
[00:00:06.xxx,xxx] <inf> wifi_app: HTTP request completed (91 bytes sent)
Next Steps
The HTTP connection works, but it is not encrypted. Anyone on the network can see the data in transit. Proceed to Part 3: HTTPS with TLS to add proper encryption and certificate verification.
Further Reading
- Zephyr BSD Sockets API - socket interface documentation
- Zephyr HTTP Client API - the
http_client_req()function and related structures - Zephyr DNS Resolver - DNS configuration and API
- JSONPlaceholder API - the free test API used in this tutorial
- Zephyr Sockets Sample - official HTTP client sample code