Overview
POST submits data to the server (device reporting, form submission, login authentication); the data goes in the request message body (the "package" that carries data in a request), which suits large, sensitive or structured data better than GET. This tutorial demonstrates: submitting hello=wb2v1 via POST to httpbin.org/post, and the server echoes back the received data.
In plain words: HTTP is like ordering at a restaurant; POST is "filling out an order form and handing it in" — hand the data (e.g. sensor readings) to the server together with "what format and how long this form is" (Content-Type/Content-Length), the server takes it and reads the form content back to you (echo) to confirm receipt. Unlike GET ("asking for a document"), POST is "handing in a document".
This tutorial is based on the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, version
release_bl_iot_sdk_1.6.40).⚠️ The official SDK does not ship a standalone POST example project. This tutorial reworks the official example
applications/protocols/http_client_socket(the GET version); the only change is the request message (method line and request body) — the rest matches the official code, marked with comments.
The official SDK has no dedicated POST project, so rework the official http_client_socket project directly:
cd ~/Ai-Thinker-WB2/applications/protocols/http_client_socket
Note:
cdis the “change directory” command, entering the official example project; all subsequentmakecommands must run in this directory.
💡 Copy the directory (e.g.
http_client_post) before modifying, keeping the official project untouched.
Open http_client_socket/main.c and change the SSID/password at the top (the official default is AIOT@FAE — change it to your own router).
Open http_client_socket/demo.c and change the target server to a site that echoes POST data:
#define WEB_SERVER "httpbin.org"
#define WEB_PORT "80"
#define WEB_PATH "/post"
💡
httpbin.org/postis a free POST test endpoint — it echoes the received data as JSON. You can also use your own server (a Python/Node service on a LAN computer).
Rework the request message to POST based on the official demo.c. The full code for this step has been moved to the end of this page:
📜 Full Code — in the “Full Code” section below, collapsed by default — click to expand, matching the official example (
applications/protocols/http_client_socket/http_client_socket/demo.c) except the request message reworked to POST per this page.
Code highlights (differences vs the official GET version):
| Change | Description |
|---|---|
POST /post HTTP/1.0 |
Method line GET → POST, path pointing at the server’s submit endpoint |
Content-Type: application/x-www-form-urlencoded |
Tells the server what format the body is (key=value&k2=v2 form); without stating it, the server can’t parse it |
Content-Length: 11 |
Must equal the actual body byte count; the server relies on it to find the body boundary — a wrong value hangs the request |
hello=wb2v1 (11 bytes) |
The request body (the data actually submitted); its byte count must match Content-Length |
| Everything else | Identical to the official http_client_socket (DNS/socket/connect/send/receive/timeout) |
💡 GET vs POST: GET appends parameters to the URL (
?a=1&b=2) with no request body; POST puts the data in the request body and must setContent-Lengthcorrectly. The board’swrite()sends the whole message at once (request line + headers + body), and the server parses it line by line.
Build in the project directory:
make -j8
Note:
makeis the “build” command, turning code into firmware (the program flashed into the board) the board can run;-j8builds with 8 parallel cores, faster.
On success a firmware build_out/http_client_socket.bin is generated.
Keep the board connected via USB, confirm the serial device, and flash:
make flash p=/dev/ttyUSB0 b=921600
Note:
make flashis the “flash” command, writing the compiled firmware into the board. Afterp=comes the serial device (often/dev/ttyUSB0on Linux,COM3-like on Windows — use your computer’s actual one),b=921600is the flash baud rate (serial transfer speed), keep the default.
⏳ During flashing, press and hold the EN button on the board when prompted to enter download mode; wait for the progress bar to complete — that means the flash succeeded.
After flashing, the board automatically restarts and runs. The serial (baud rate 921600) prints the connection logs, then httpbin.org’s JSON echo:
DNS lookup succeeded. IP=34.224.xxx.xxx
... connected
... socket send success
HTTP/1.1 200 OK
Content-Type: application/json
...
{
"args": {},
"data": "hello=wb2v1",
"form": {
"hello": "wb2v1"
},
...
}
... done reading from socket. Last read return=0 errno=0
HTTP/1.1 200 OK and "data": "hello=wb2v1" appearing means the POST succeeded — the server fully received the request body. If you only see GOT IP without the echo, first confirm the board is online (GOT IP), your computer’s browser can open httpbin.org (target server reachable), then troubleshoot the message — see the FAQ at the end.
💡 LAN self-test: run a simple service in Python on the computer (
python3 -m http.serveronly supports GET — useflaskor Node to write a POST endpoint instead), setWEB_SERVERto the computer’s IP andWEB_PATHto the endpoint path, and verify against your own server.
API Summary for This Tutorial
getaddrinfo(name, port, &hints, &res)
Resolves a domain name into a linked list of IPs.
Parameters:
name: domain or IP string, e.g."httpbin.org"port: port string, e.g."80"hints: query criteria structure (limits to IPv4 + TCP)res: output parameter, resolved linked-list pointer
Return: 0 on success; negative error code on failure
freeaddrinfo(res)
Frees the resolution result.
Parameters:
res: result linked list returned bygetaddrinfo
Return: none
socket(af, type, proto)
Creates a TCP socket.
Parameters:
af:AF_INET(IPv4)type:SOCK_STREAM(streaming TCP)proto: pass0
Return: socket descriptor on success; -1 on failure
connect(fd, addr, addrlen)
Connects to the server.
Parameters:
fd: socket descriptoraddr: server address (struct sockaddr*)addrlen: address length
Return: 0 on success; negative error code on failure
write(fd, buf, len)
Sends the whole HTTP message (including the request body).
Parameters:
fd: socket descriptorbuf: request message buffer (request line + headers + blank line + body)len: total message length
Return: bytes sent on success; negative on failure
read(fd, buf, len)
Reads the server response.
Parameters:
fd: socket descriptorbuf: receive bufferlen: buffer length
Return: bytes read; 0 = peer closed; negative = error
close(fd)
Closes the connection.
Parameters:
fd: socket descriptor
Return: 0 on success; -1 on failure
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, len)
Sets the receive timeout.
Parameters:
fd: socket descriptorlevel:SOL_SOCKEToptname:SO_RCVTIMEOtv:struct timevalpointerlen:sizeof(struct timeval)
Return: 0 on success; negative error code on failure
inet_ntoa(addr)
Converts an IP address to a string.
Parameters:
addr: network-byte-order IP
Return: dotted-decimal string
bl_putchar(c)
Outputs one character to the serial.
Parameters:
c: the character to output
Return: none
📌 Request body field meanings:
Content-Typedeclares the data format (formapplication/x-www-form-urlencoded, JSONapplication/json, etc.);Content-Lengthdeclares the body byte count — a wrong value makes the server fail to parse or the request hang, so always update it after changing the body.
Full Code
Below is the complete demo.c source, matching the official example (applications/protocols/http_client_socket/http_client_socket/demo.c) except the request message reworked to POST per this page:
📜 Click to expand the full demo.c code
#include <stdio.h>
#include <FreeRTOS.h>
#include <task.h>
#include <lwip/sockets.h>
#include <lwip/netdb.h>
#include <lwip/tcp.h>
#include <lwip/err.h>
#include <http_client.h>
#include <cli.h>
#include "demo.h"
#include <blog.h>
#define WEB_SERVER "httpbin.org"
#define WEB_PORT "80"
#define WEB_PATH "/post"
/* ★ Rework point ①: method GET → POST, add Content-Type / Content-Length and request body */
static const char *REQUEST = "POST " WEB_PATH " HTTP/1.0\r\n"
"Host: " WEB_SERVER ":" WEB_PORT "\r\n"
"User-Agent: aithinker wb2\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 11\r\n"
"\r\n"
"hello=wb2v1"; /* ★ Rework point ②: request body, 11 bytes */
void http_get_task(void *pvParameters)
{
const struct addrinfo hints = {
.ai_family = AF_INET,
.ai_socktype = SOCK_STREAM,
};
struct addrinfo *res;
struct in_addr *addr;
int s, r;
char recv_buf[4096];
while (1)
{
int err = getaddrinfo(WEB_SERVER, "80", &hints, &res);
if (err != 0 || res == NULL)
{
blog_error("DNS lookup failed err=%d res=%p", err, res);
vTaskDelay(1000 / portTICK_PERIOD_MS);
continue;
}
addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
blog_info("DNS lookup succeeded. IP=%s", inet_ntoa(*addr));
s = socket(res->ai_family, res->ai_socktype, 0);
if (s < 0)
{
blog_error("... Failed to allocate socket.");
freeaddrinfo(res);
vTaskDelay(1000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... allocated socket");
if (connect(s, res->ai_addr, res->ai_addrlen) != 0)
{
blog_error("... socket connect failed errno=%d", errno);
close(s);
freeaddrinfo(res);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... connected");
freeaddrinfo(res);
if (write(s, REQUEST, strlen(REQUEST)) < 0)
{
blog_error("... socket send failed");
close(s);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... socket send success");
struct timeval receiving_timeout;
receiving_timeout.tv_sec = 5;
receiving_timeout.tv_usec = 0;
if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout,
sizeof(receiving_timeout)) < 0)
{
blog_error("... failed to set socket receiving timeout");
close(s);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... set socket receiving timeout success");
// FIXME fix putchar
extern int bl_putchar(int c);
/* Read HTTP response */
do
{
bzero(recv_buf, sizeof(recv_buf));
r = read(s, recv_buf, sizeof(recv_buf) - 1);
for (int i = 0; i < r; i++)
{
bl_putchar(recv_buf[i]);
}
} while (r > 0);
blog_info("... done reading from socket. Last read return=%d errno=%d\r\n", r, errno);
close(s);
for (int countdown = 10; countdown >= 0; countdown--)
{
blog_info("%d... ", countdown);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
blog_info("Starting again!");
}
}FAQ & Troubleshooting
⚠️ Server returns 405 Method Not Allowed
Cause: the target path doesn't support POST (e.g. a static site's root path only allows GET)
Fix: use a POST-capable endpoint (httpbin.org/post, httpbin.org/anything) or your own server
⚠️ Server receives an empty body / request hangs
Cause: Content-Length doesn't match the actual body byte count (Chinese characters count 3 bytes in UTF-8; spaces count too)
Fix: after changing the body, count the bytes one by one (verify with strlen("hello=wb2v1")) and keep Content-Length in sync
⚠️ Submitted Chinese garbled
Cause: the Chinese wasn't URL-encoded, and its byte count doesn't match Content-Length
Fix: URL-encode the Chinese first (e.g. %E4%BD%A0%E5%A5%BD), then set Content-Length to the encoded byte count
⚠️ Other issues (can't connect to router / DNS failure / timeout / stack overflow)
Note: identical to HTTP GET
Fix: follow the pitfalls in HTTP GET (including can't connect to the router, serial not found, flashing failure, etc.)
⚠️ Serial device not found / can't open
Cause: USB-to-serial driver not installed, insufficient permission, or the cable only charges and can't transfer data
Fix: on Linux check the device with lsusb/dmesg; if permission denied run sudo chmod 666 /dev/ttyUSB0; on Windows install the driver and check the COM port in Device Manager; try a data-capable cable
⚠️ Flashing keeps waiting / fails
Cause: download mode wasn't entered, wrong baud rate, or a wrong serial number
Fix: press and hold EN during flashing to enter download mode as prompted; change p=/dev/ttyUSB0 to your actual serial port; try another USB port or cable
Self-Check
"data": "hello=wb2v1" appears in the JSON echo on the serial — the POST submission is verified.

