Skip to content

File espos_sk_http.h

File List > espos_sk > include > espos_sk_http.h

Go to the documentation of this file

/*
 * SPDX-FileCopyrightText: 2026 Dirk Wahrheit
 * SPDX-License-Identifier: Apache-2.0
 *
 * espos_sk_http — one correct way to talk HTTP to the selected SignalK
 * server: GET/PUT/POST/DELETE with the access token, URLs that follow the
 * server's scheme, and the two lookups a display needs most (a path's
 * current value and its meta).
 *
 * Why a helper and not "just use esp_http_client": four hand-rolled copies
 * of this code in one firmware had two of them rebooting the device. Both
 * faults were seen on the ESP32-P4 and both are in esp_http_client itself:
 *
 *   1. esp_http_client_open() / fetch_headers() / read() leaves the client's
 *      cache_data_in_fetch_hdr flag set, and a body that arrives in the same
 *      segment as the headers -- every small SignalK reply -- then trips
 *      assert(orig_raw_data == raw_data) in http_on_body. Only perform()
 *      clears the flag.
 *   2. Reusing one handle across esp_http_client_perform() calls desyncs the
 *      same two pointers and hits the same assert mid-batch.
 *
 * Contract (every function below):
 *   - a fresh esp_http_client per call and esp_http_client_perform() only;
 *     never open/fetch_headers/read, never a reused handle;
 *   - the body is collected in HTTP_EVENT_ON_DATA and bounded by max_body:
 *     beyond it `truncated` is set and the rest is discarded -- never parse
 *     a truncated body, treat it as "the reply was too big";
 *   - `Authorization: Bearer <token>` from espos_sk_get_token(), snapshotted
 *     per call; the token is never put in a query string. No token = no
 *     header, which is right for a server running without security;
 *   - scheme from the selected server's tls flag (http/ws, or https/wss with
 *     the same certificate-bundle rules as the delta stream, docs/signalk.md);
 *   - 401/403 while a token was sent -> espos_sk_report_unauthorized(), so
 *     the token machine re-verifies and re-requests, unless opted out;
 *   - ESP_ERR_INVALID_STATE when no server is selected;
 *   - requests in flight are bounded by Kconfig ESPOS_SK_HTTP_MAX_CONCURRENT
 *     (default 2): each costs a socket and, over TLS, ~20 KB of RAM. A caller
 *     over the limit waits up to its own timeout_ms for a slot.
 *
 * Threading: BLOCKING, for up to timeout_ms waiting for a slot plus
 * timeout_ms on the wire, on the caller's task, with ~2 KiB of its stack.
 * Call from an application task. Never from the SK stream task (the
 * espos_sk_subscribe / espos_sk_put callbacks), from an ESPOS_EVENT handler,
 * from a Bluetooth stack callback, or from an HTTP URI handler (it stalls
 * the web UI). All functions are thread-safe.
 */
#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"

#ifdef __cplusplus
extern "C" {
#endif

#define ESPOS_SK_URL_MAX 384

typedef struct {
    int status;      /* HTTP status; 0 when no reply arrived (see err) */
    esp_err_t err;   /* the function's return value, kept with the reply */
    char *body;      /* malloc'ed, NUL-terminated, "" for an empty reply; NULL when err != ESP_OK */
    size_t len;      /* bytes in body (excluding the NUL) */
    bool truncated;  /* body hit max_body: len == max_body, the rest was discarded */
    /* The TLS handshake refused the server: its certificate is not the one
     * this device trusts. status is 0 and err is a transport error, which on
     * their own read as "unreachable" and would be retried with a five-minute
     * backoff over something an operator fixes in ten seconds. cert_reason
     * says which way it differed. */
    bool cert_error;
    char cert_reason[64];
} espos_sk_http_resp_t;

typedef struct {
    uint32_t timeout_ms;          /* 0 = 6000 */
    size_t max_body;              /* 0 = 16384 */
    bool no_auth;                 /* omit the Authorization header even when a token exists */
    bool no_report_unauthorized;  /* a 401/403 does not touch the token machine */
    const char *accept;           /* NULL = "application/json"; "" = no Accept header */
} espos_sk_http_opts_t;

esp_err_t espos_sk_http_get(const char *path, const espos_sk_http_opts_t *o, espos_sk_http_resp_t *r);
esp_err_t espos_sk_http_put(const char *path, const char *json, const espos_sk_http_opts_t *o, espos_sk_http_resp_t *r);
esp_err_t espos_sk_http_post(const char *path, const char *json, const espos_sk_http_opts_t *o, espos_sk_http_resp_t *r);
esp_err_t espos_sk_http_delete(const char *path, const espos_sk_http_opts_t *o, espos_sk_http_resp_t *r);
void espos_sk_http_resp_free(espos_sk_http_resp_t *r);

esp_err_t espos_sk_url(const char *path, char *out, size_t n);
esp_err_t espos_sk_ws_url(const char *path, char *out, size_t n);

esp_err_t espos_sk_get_value(const char *sk_path, char **out_json);
esp_err_t espos_sk_get_meta(const char *sk_path, char **out_json);

#ifdef __cplusplus
}
#endif