Merge pull request 'dev → stage: binary assets, payment fix, checkout layout' (#129) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 7m4s
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 7m4s
This commit was merged in pull request #129.
This commit is contained in:
+63
-2
@@ -77,6 +77,23 @@ static _Thread_local int _tl_arena_active = 0;
|
||||
* Allows serving PNGs and other binary files without strlen truncation. */
|
||||
static _Thread_local size_t _tl_fs_read_len = 0;
|
||||
|
||||
/* Binary body side-channel for http_response().
|
||||
*
|
||||
* http_response() normally JSON-encodes the body via jb_emit_escaped(), which
|
||||
* stops at the first null byte (C-string semantics). Binary files like PNGs
|
||||
* contain null bytes as early as byte 8 (IHDR chunk length), causing truncation.
|
||||
*
|
||||
* When _tl_fs_read_len > 0 at the time http_response() is called, we skip
|
||||
* JSON-encoding and instead:
|
||||
* 1. malloc-copy the raw bytes here
|
||||
* 2. write the sentinel string "__el_binary__" into the envelope body field
|
||||
* 3. In http_send_response(), detect the sentinel and use these raw bytes
|
||||
*
|
||||
* Thread-local so each worker thread has independent storage.
|
||||
* Lifecycle: set by http_response(), consumed (and freed) by http_send_response(). */
|
||||
static _Thread_local char* _tl_binary_body = NULL;
|
||||
static _Thread_local size_t _tl_binary_size = 0;
|
||||
|
||||
static void el_arena_track(char* p) {
|
||||
if (!_tl_arena_active || !p) return;
|
||||
if (_tl_arena.count >= _tl_arena.cap) {
|
||||
@@ -1536,10 +1553,22 @@ static void http_send_response(int fd, const char* body) {
|
||||
}
|
||||
|
||||
const char* eff_body = is_envelope ? env_body : body;
|
||||
int binary_side_channel = 0;
|
||||
|
||||
/* Binary side-channel: if the envelope body is the sentinel "__el_binary__",
|
||||
* http_response() stored the real bytes in _tl_binary_body/_tl_binary_size.
|
||||
* Substitute them here so http_send_all() sends the correct binary payload. */
|
||||
if (is_envelope && env_body && strcmp(env_body, "__el_binary__") == 0
|
||||
&& _tl_binary_body && _tl_binary_size > 0) {
|
||||
eff_body = _tl_binary_body;
|
||||
binary_side_channel = 1;
|
||||
}
|
||||
|
||||
/* Use the real byte count from fs_read if available (handles binary files
|
||||
* with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for
|
||||
* normal text/JSON responses where _tl_fs_read_len is 0. */
|
||||
size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
|
||||
size_t blen = binary_side_channel ? _tl_binary_size
|
||||
: (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
|
||||
_tl_fs_read_len = 0; /* consume — one-shot per response */
|
||||
int head_only = _tl_http_head_only;
|
||||
|
||||
@@ -1587,6 +1616,13 @@ static void http_send_response(int fd, const char* body) {
|
||||
if (env_parsed_root) el_release(env_parsed_root);
|
||||
free(env_body);
|
||||
free(hdrs.buf);
|
||||
|
||||
/* Release binary side-channel if it was used (or left over from an error). */
|
||||
if (_tl_binary_body) {
|
||||
free(_tl_binary_body);
|
||||
_tl_binary_body = NULL;
|
||||
_tl_binary_size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -1961,6 +1997,14 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
|
||||
const char* b = EL_CSTR(body);
|
||||
if (!b) b = "";
|
||||
|
||||
/* Capture binary length BEFORE clearing _tl_fs_read_len.
|
||||
* If the body came from fs_read(), _tl_fs_read_len holds the real byte
|
||||
* count. jb_emit_escaped() stops at the first NUL byte, so we cannot
|
||||
* JSON-encode binary data directly. Instead we copy it to a thread-local
|
||||
* side-channel and write the sentinel "__el_binary__" into the envelope.
|
||||
* http_send_response() detects the sentinel and uses the side-channel. */
|
||||
size_t binary_len = _tl_fs_read_len;
|
||||
|
||||
/* Clear the fs_read binary-length hint: the envelope we're about to build
|
||||
* is a fresh JSON string, not the raw file bytes. Without this reset,
|
||||
* http_worker would use the stale _tl_fs_read_len (= original file size)
|
||||
@@ -1968,6 +2012,18 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
|
||||
* http_send_response and http_parse_envelope. */
|
||||
_tl_fs_read_len = 0;
|
||||
|
||||
if (binary_len > 0) {
|
||||
/* Binary body path: store raw bytes in thread-local, emit sentinel. */
|
||||
free(_tl_binary_body); /* discard any stale binary from a prior error path */
|
||||
_tl_binary_body = malloc(binary_len);
|
||||
if (_tl_binary_body) {
|
||||
memcpy(_tl_binary_body, b, binary_len);
|
||||
_tl_binary_size = binary_len;
|
||||
} else {
|
||||
_tl_binary_size = 0; /* malloc failed — fall through to empty body */
|
||||
}
|
||||
}
|
||||
|
||||
JsonBuf out; jb_init(&out);
|
||||
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
|
||||
jb_puts(&out, ",\"status\":");
|
||||
@@ -1977,7 +2033,12 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
|
||||
jb_puts(&out, ",\"headers\":");
|
||||
jb_puts(&out, hj);
|
||||
jb_puts(&out, ",\"body\":");
|
||||
jb_emit_escaped(&out, b);
|
||||
if (binary_len > 0 && _tl_binary_body) {
|
||||
/* Sentinel: http_send_response() will substitute the real bytes. */
|
||||
jb_puts(&out, "\"__el_binary__\"");
|
||||
} else {
|
||||
jb_emit_escaped(&out, b);
|
||||
}
|
||||
jb_putc(&out, '}');
|
||||
return el_wrap_str(out.buf);
|
||||
}
|
||||
|
||||
+25
-1
@@ -345,7 +345,31 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
}
|
||||
|
||||
fn checkout_style_html() -> String {
|
||||
let css: String = ".checkout-plan-name {
|
||||
let css: String = ".checkout-shell {
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
align-items: start;
|
||||
}
|
||||
.checkout-summary {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
}
|
||||
.checkout-form-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.checkout-shell {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
.checkout-summary {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
.checkout-plan-name {
|
||||
font-family: var(--head);
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: 600;
|
||||
|
||||
+2
-2
@@ -698,12 +698,12 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
||||
}
|
||||
|
||||
// Free tier: $0 PaymentIntent for age verification (18+ requirement).
|
||||
// Card is verified and saved (setup_future_usage=off_session). No charge.
|
||||
// Verifies card is valid. No charge, no capture.
|
||||
// Note: setup_future_usage cannot be used with amount=0.
|
||||
if str_eq(plan, "free") {
|
||||
let free_pi_body: String = "amount=0"
|
||||
+ "¤cy=usd"
|
||||
+ "&payment_method_types[]=card"
|
||||
+ "&setup_future_usage=off_session"
|
||||
+ "&metadata[plan]=free"
|
||||
+ "&metadata[purpose]=age_verification"
|
||||
let free_pi_body = if !str_eq(pi_cus_id, "") { free_pi_body + "&customer=" + pi_cus_id } else { free_pi_body }
|
||||
|
||||
Reference in New Issue
Block a user