Skip to content

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.

🎯Page GoalBuild a POST request message on top of the official GET example and verify the server echo, mastering the request body, Content-Length and Content-Type.
🧰Prerequisites① Ai-WB2 development board (Type-C data cable) ② A 2.4GHz router (with public internet access) ③ Environment set up per [SDK Installation](../../sdk/sdk_intro) and completed [Connect Wi-Fi](./wifi_connect) and [HTTP GET](./http_get).
🔗RelatedGET requests: [HTTP GET](./http_get); updating resources: [HTTP PUT](./http_put).

Enter the Example Project

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: cd is the “change directory” command, entering the official example project; all subsequent make commands must run in this directory.

💡 Copy the directory (e.g. http_client_post) before modifying, keeping the official project untouched.

Modify the Router Parameters and Target Address

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/post is 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).

Write the Code (Rework Point: Request Message)

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 GETPOST, 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 set Content-Length correctly. The board’s write() sends the whole message at once (request line + headers + body), and the server parses it line by line.

Build the Project

Build in the project directory:

make -j8

Note: make is the “build” command, turning code into firmware (the program flashed into the board) the board can run; -j8 builds with 8 parallel cores, faster.

On success a firmware build_out/http_client_socket.bin is generated.

Flash the Firmware

Keep the board connected via USB, confirm the serial device, and flash:

make flash p=/dev/ttyUSB0 b=921600

Note: make flash is the “flash” command, writing the compiled firmware into the board. After p= comes the serial device (often /dev/ttyUSB0 on Linux, COM3-like on Windows — use your computer’s actual one), b=921600 is 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.

Run and Verify

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.server only supports GET — use flask or Node to write a POST endpoint instead), set WEB_SERVER to the computer’s IP and WEB_PATH to 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 by getaddrinfo

Return: none

socket(af, type, proto)

Creates a TCP socket.

Parameters:

  • af: AF_INET (IPv4)
  • type: SOCK_STREAM (streaming TCP)
  • proto: pass 0

Return: socket descriptor on success; -1 on failure

connect(fd, addr, addrlen)

Connects to the server.

Parameters:

  • fd: socket descriptor
  • addr: 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 descriptor
  • buf: 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 descriptor
  • buf: receive buffer
  • len: 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 descriptor
  • level: SOL_SOCKET
  • optname: SO_RCVTIMEO
  • tv: struct timeval pointer
  • len: 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-Type declares the data format (form application/x-www-form-urlencoded, JSON application/json, etc.); Content-Length declares 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
c
#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.

Released under the MIT License. Build Time 2026-09-11 14:52:23