MIRROR: javascript for ๐Ÿœ's, a tiny runtime with big ambitions
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add fetch redirect support

+392 -3
+1
include/modules/response.h
··· 46 46 int status, 47 47 const char *status_text, 48 48 const char *url, 49 + int url_list_size, 49 50 ant_value_t headers_obj, 50 51 const uint8_t *body, 51 52 size_t body_len,
+205 -2
src/modules/fetch.c
··· 38 38 ant_http_request_t *http_req; 39 39 40 40 int refs; 41 + int redirect_count; 41 42 bool settled; 42 43 bool aborted; 44 + bool restart_pending; 43 45 bool response_started; 44 46 } fetch_request_t; 45 47 46 48 static UT_array *pending_requests = NULL; 49 + static const int k_fetch_max_redirects = 20; 50 + static void fetch_start_http(fetch_request_t *req); 47 51 48 52 static void fetch_request_retain(fetch_request_t *req) { 49 53 if (req) req->refs++; ··· 92 96 return reason; 93 97 } 94 98 return value; 99 + } 100 + 101 + static bool fetch_is_redirect_status(int status) { 102 + return 103 + status == 301 || 104 + status == 302 || 105 + status == 303 || 106 + status == 307 || 107 + status == 308; 95 108 } 96 109 97 110 static void fetch_cancel_request_body(fetch_request_t *req, ant_value_t reason) { ··· 134 147 static char *fetch_build_request_url(request_data_t *request) { 135 148 if (!request) return NULL; 136 149 return build_href(&request->url); 150 + } 151 + 152 + static const char *fetch_find_header_value(const ant_http_header_t *headers, const char *name) { 153 + for (const ant_http_header_t *entry = headers; entry; entry = entry->next) { 154 + if (entry->name && strcasecmp(entry->name, name) == 0) return entry->value; 155 + } 156 + return NULL; 157 + } 158 + 159 + static bool fetch_redirect_rewrites_to_get(int status, const char *method) { 160 + if (!method) return false; 161 + if (status == 303) return strcasecmp(method, "HEAD") != 0; 162 + return (status == 301 || status == 302) && strcasecmp(method, "POST") == 0; 163 + } 164 + 165 + typedef struct { 166 + ant_t *js; 167 + ant_value_t headers; 168 + bool drop_body_headers; 169 + bool failed; 170 + } fetch_redirect_headers_ctx_t; 171 + 172 + static void fetch_copy_redirect_header(const char *name, const char *value, void *ctx) { 173 + fetch_redirect_headers_ctx_t *copy = (fetch_redirect_headers_ctx_t *)ctx; 174 + ant_value_t step = 0; 175 + 176 + if (!copy || copy->failed) return; 177 + if (copy->drop_body_headers && name && strcasecmp(name, "content-type") == 0) return; 178 + 179 + step = headers_append_literal(copy->js, copy->headers, name, value); 180 + if (is_err(step)) copy->failed = true; 181 + } 182 + 183 + static ant_value_t fetch_replace_request_headers(fetch_request_t *req, bool drop_body_headers) { 184 + ant_t *js = req->js; 185 + 186 + request_data_t *request = request_get_data(req->request_obj); 187 + ant_value_t current = request_get_headers(req->request_obj); 188 + ant_value_t headers = headers_create_empty(js); 189 + 190 + fetch_redirect_headers_ctx_t ctx = { 191 + .js = js, 192 + .headers = headers, 193 + .drop_body_headers = drop_body_headers, 194 + .failed = false, 195 + }; 196 + 197 + if (is_err(headers)) return headers; 198 + headers_for_each(current, fetch_copy_redirect_header, &ctx); 199 + if (ctx.failed) return js_mkerr(js, "out of memory"); 200 + 201 + headers_set_guard(headers, 202 + strcmp(request->mode, "no-cors") == 0 203 + ? HEADERS_GUARD_REQUEST_NO_CORS 204 + : HEADERS_GUARD_REQUEST 205 + ); 206 + 207 + headers_apply_guard(headers); 208 + js_set_slot_wb(js, req->request_obj, SLOT_REQUEST_HEADERS, headers); 209 + 210 + return js_mkundef(); 211 + } 212 + 213 + static ant_value_t fetch_clear_redirect_request_body(fetch_request_t *req) { 214 + request_data_t *request = request_get_data(req->request_obj); 215 + ant_value_t headers_step = 0; 216 + 217 + if (!request) 218 + return fetch_type_error(req->js, "Invalid Request object"); 219 + 220 + free(request->body_data); 221 + free(request->body_type); 222 + request->body_data = NULL; 223 + request->body_size = 0; 224 + request->body_type = NULL; 225 + request->body_is_stream = false; 226 + request->has_body = false; 227 + request->body_used = false; 228 + js_set_slot_wb(req->js, req->request_obj, SLOT_REQUEST_BODY_STREAM, js_mkundef()); 229 + 230 + headers_step = fetch_replace_request_headers(req, true); 231 + if (is_err(headers_step)) return headers_step; 232 + 233 + return js_mkundef(); 234 + } 235 + 236 + static ant_value_t fetch_set_redirect_method(fetch_request_t *req, const char *method) { 237 + request_data_t *request = request_get_data(req->request_obj); 238 + char *dup = NULL; 239 + 240 + if (!request) return fetch_type_error(req->js, "Invalid Request object"); 241 + dup = strdup(method); 242 + if (!dup) return js_mkerr(req->js, "out of memory"); 243 + free(request->method); 244 + request->method = dup; 245 + return js_mkundef(); 246 + } 247 + 248 + static ant_value_t fetch_update_request_url(fetch_request_t *req, const char *location) { 249 + request_data_t *request = request_get_data(req->request_obj); 250 + url_state_t next = {0}; 251 + char *base = NULL; 252 + 253 + if (!request || !location) return fetch_type_error(req->js, "Invalid redirect URL"); 254 + base = fetch_build_request_url(request); 255 + if (!base) return fetch_type_error(req->js, "Invalid request URL"); 256 + 257 + if (parse_url_to_state(location, base, &next) != 0) { 258 + free(base); 259 + url_state_clear(&next); 260 + return fetch_type_error(req->js, "Invalid redirect URL"); 261 + } 262 + 263 + free(base); 264 + url_state_clear(&request->url); 265 + request->url = next; 266 + return js_mkundef(); 267 + } 268 + 269 + static ant_value_t fetch_prepare_redirect(fetch_request_t *req, const ant_http_response_t *resp) { 270 + request_data_t *request = request_get_data(req->request_obj); 271 + const char *location = fetch_find_header_value(resp->headers, "location"); 272 + ant_value_t step = 0; 273 + bool rewrite_to_get = false; 274 + 275 + if (!request || !location || location[0] == '\0') return js_mkundef(); 276 + if (req->redirect_count >= k_fetch_max_redirects) { 277 + return fetch_type_error(req->js, "fetch failed: too many redirects"); 278 + } 279 + 280 + rewrite_to_get = fetch_redirect_rewrites_to_get(resp->status, request->method); 281 + if (!rewrite_to_get && request->body_is_stream) { 282 + return fetch_type_error(req->js, "fetch failed: cannot follow redirect with a streamed request body"); 283 + } 284 + 285 + if (rewrite_to_get) { 286 + step = fetch_set_redirect_method(req, strcasecmp(request->method, "HEAD") == 0 ? "HEAD" : "GET"); 287 + if (is_err(step)) return step; 288 + step = fetch_clear_redirect_request_body(req); 289 + if (is_err(step)) return step; 290 + } 291 + 292 + step = fetch_update_request_url(req, location); 293 + if (is_err(step)) return step; 294 + 295 + req->redirect_count++; 296 + req->restart_pending = true; 297 + return js_mkundef(); 137 298 } 138 299 139 300 typedef struct { ··· 283 444 fetch_request_t *req = (fetch_request_t *)user_data; 284 445 285 446 ant_t *js = req->js; 447 + request_data_t *request = request_get_data(req->request_obj); 448 + 286 449 ant_value_t headers = 0; 450 + ant_value_t step = 0; 287 451 ant_value_t stream = 0; 288 452 ant_value_t response = 0; 289 453 290 454 char *url = NULL; 291 455 if (req->aborted) return; 456 + if (!request) { 457 + fetch_reject(req, fetch_type_error(js, "Invalid Request object")); 458 + ant_http_request_cancel(http_req); 459 + return; 460 + } 461 + 462 + if (fetch_is_redirect_status(resp->status)) { 463 + const char *location = fetch_find_header_value(resp->headers, "location"); 464 + const char *redirect_mode = request->redirect ? request->redirect : "follow"; 465 + 466 + if (location && location[0] != '\0' && strcmp(redirect_mode, "error") == 0) { 467 + fetch_reject(req, fetch_type_error(js, "fetch failed: redirect mode is set to error")); 468 + ant_http_request_cancel(http_req); 469 + return; 470 + } 471 + 472 + if (strcmp(redirect_mode, "follow") == 0) { 473 + step = fetch_prepare_redirect(req, resp); 474 + 475 + if (is_err(step)) { 476 + fetch_reject(req, fetch_rejection_reason(js, step)); 477 + ant_http_request_cancel(http_req); 478 + return; 479 + } 480 + 481 + if (req->restart_pending) { 482 + ant_http_request_cancel(http_req); 483 + return; 484 + }} 485 + } 292 486 293 487 headers = fetch_headers_from_http(js, resp->headers); 294 488 if (is_err(headers)) { ··· 306 500 307 501 url = fetch_build_request_url(request_get_data(req->request_obj)); 308 502 response = response_create_fetched( 309 - js, resp->status, resp->status_text, url, headers, NULL, 0, stream, NULL 503 + js, resp->status, resp->status_text, url, 504 + req->redirect_count + 1, headers, NULL, 0, stream, NULL 310 505 ); 506 + 311 507 free(url); 312 508 313 509 if (is_err(response)) { ··· 370 566 ant_value_t reason = 0; 371 567 req->http_req = NULL; 372 568 569 + if (req->restart_pending) { 570 + req->restart_pending = false; 571 + fetch_start_http(req); 572 + return; 573 + } 574 + 373 575 if (result != ANT_HTTP_RESULT_OK || error_code != 0) { 374 576 reason = fetch_transport_reason(req, result, error_message); 375 577 if (is_object_type(req->response_obj)) fetch_error_response_body(req, reason); ··· 435 637 436 638 headers_set_literal(js, headers, "content-type", content_type); 437 639 response = response_create_fetched( 438 - js, 200, "OK", url, headers, (const uint8_t *)body, len, js_mkundef(), content_type 640 + js, 200, "OK", url, 1, headers, 641 + (const uint8_t *)body, len, js_mkundef(), content_type 439 642 ); 440 643 441 644 free(url);
+2 -1
src/modules/response.c
··· 1142 1142 int status, 1143 1143 const char *status_text, 1144 1144 const char *url, 1145 + int url_list_size, 1145 1146 ant_value_t headers_obj, 1146 1147 const uint8_t *body, 1147 1148 size_t body_len, ··· 1168 1169 url_state_clear(&resp->url); 1169 1170 resp->url = parsed; 1170 1171 resp->has_url = true; 1171 - resp->url_list_size = 1; 1172 + resp->url_list_size = url_list_size > 0 ? url_list_size : 1; 1172 1173 } else url_state_clear(&parsed); 1173 1174 1174 1175 if (rs_is_stream(body_stream)) {
+68
tests/class_vs_proto.js
··· 1 + var SequenceProto = function (start) { 2 + this.item = start; 3 + return this; 4 + }; 5 + 6 + SequenceProto.prototype.next = function () { 7 + var temp = this.item; 8 + this.item = temp + 2; 9 + return temp; 10 + }; 11 + 12 + class SequenceClass { 13 + constructor(start) { 14 + this.item = start; 15 + } 16 + next() { 17 + const temp = this.item; 18 + this.item = temp + 2; 19 + return temp; 20 + } 21 + } 22 + 23 + const ITERATIONS = 1_000_000; 24 + const WARMUP = 100_000; 25 + 26 + function runProto() { 27 + const seq = new SequenceProto(1); 28 + let num = 0; 29 + for (let i = 0; i < ITERATIONS; i++) { 30 + num = seq.next(); 31 + } 32 + return num; 33 + } 34 + 35 + function runClass() { 36 + const seq = new SequenceClass(1); 37 + let num = 0; 38 + for (let i = 0; i < ITERATIONS; i++) { 39 + num = seq.next(); 40 + } 41 + return num; 42 + } 43 + 44 + for (let i = 0; i < WARMUP; i++) { 45 + new SequenceProto(1).next(); 46 + new SequenceClass(1).next(); 47 + } 48 + 49 + const runs = 10; 50 + const protoTimes = []; 51 + const classTimes = []; 52 + 53 + for (let i = 0; i < runs; i++) { 54 + const t1 = performance.now(); 55 + runProto(); 56 + protoTimes.push(performance.now() - t1); 57 + 58 + const t2 = performance.now(); 59 + runClass(); 60 + classTimes.push(performance.now() - t2); 61 + } 62 + 63 + const avg = arr => arr.reduce((a, b) => a + b, 0) / arr.length; 64 + 65 + console.log(`Prototype avg: ${avg(protoTimes).toFixed(3)}ms`); 66 + console.log(`Class avg: ${avg(classTimes).toFixed(3)}ms`); 67 + console.log(`Difference: ${Math.abs(avg(protoTimes) - avg(classTimes)).toFixed(3)}ms`); 68 + console.log(`Winner: ${avg(protoTimes) < avg(classTimes) ? 'Prototype' : 'Class'}`);
+92
tests/test_fetch_redirect.cjs
··· 1 + const assert = require('node:assert'); 2 + const http = require('node:http'); 3 + 4 + const server = http.createServer(async (req, res) => { 5 + const chunks = []; 6 + for await (const chunk of req) chunks.push(chunk); 7 + 8 + const body = Buffer.concat(chunks).toString('utf8'); 9 + 10 + if (req.url === '/redirect') { 11 + res.writeHead(302, { location: '/final' }); 12 + res.end('redirecting'); 13 + return; 14 + } 15 + 16 + if (req.url === '/redirect-post') { 17 + res.writeHead(302, { location: '/final-post' }); 18 + res.end('redirecting post'); 19 + return; 20 + } 21 + 22 + if (req.url === '/redirect-307') { 23 + res.writeHead(307, { location: '/final-307' }); 24 + res.end('redirecting preserve'); 25 + return; 26 + } 27 + 28 + if (req.url === '/final') { 29 + res.writeHead(200, { 'content-type': 'text/plain' }); 30 + res.end('redirect-ok'); 31 + return; 32 + } 33 + 34 + if (req.url === '/final-post') { 35 + res.writeHead(200, { 'content-type': 'text/plain' }); 36 + res.end(`${req.method}:${body}`); 37 + return; 38 + } 39 + 40 + if (req.url === '/final-307') { 41 + res.writeHead(200, { 'content-type': 'text/plain' }); 42 + res.end(`${req.method}:${body}`); 43 + return; 44 + } 45 + 46 + res.writeHead(404); 47 + res.end('missing'); 48 + }); 49 + 50 + server.listen(0, async () => { 51 + const { port } = server.address(); 52 + const base = `http://127.0.0.1:${port}`; 53 + 54 + try { 55 + const followed = await fetch(`${base}/redirect`); 56 + assert.equal(followed.status, 200); 57 + assert.equal(followed.redirected, true); 58 + assert.equal(followed.url, `${base}/final`); 59 + assert.equal(await followed.text(), 'redirect-ok'); 60 + 61 + const rewritten = await fetch(`${base}/redirect-post`, { 62 + method: 'POST', 63 + body: 'hello-body', 64 + }); 65 + assert.equal(rewritten.status, 200); 66 + assert.equal(await rewritten.text(), 'GET:'); 67 + 68 + const preserved = await fetch(`${base}/redirect-307`, { 69 + method: 'POST', 70 + body: 'hello-again', 71 + }); 72 + assert.equal(preserved.status, 200); 73 + assert.equal(await preserved.text(), 'POST:hello-again'); 74 + 75 + const manual = await fetch(`${base}/redirect`, { redirect: 'manual' }); 76 + assert.equal(manual.status, 302); 77 + assert.equal(manual.redirected, false); 78 + assert.equal(manual.url, `${base}/redirect`); 79 + 80 + let sawRedirectError = false; 81 + try { 82 + await fetch(`${base}/redirect`, { redirect: 'error' }); 83 + } catch (error) { 84 + sawRedirectError = /redirect mode is set to error/.test(String(error)); 85 + } 86 + assert.equal(sawRedirectError, true); 87 + 88 + console.log('ok'); 89 + } finally { 90 + server.close(); 91 + } 92 + });
+24
tests/test_url_bracket_query.cjs
··· 1 + function assert(condition, message) { 2 + if (!condition) throw new Error(message); 3 + } 4 + 5 + const input = 'https://e621.net/pools.json?search[id]=14032,20025,26727'; 6 + 7 + const url = new URL(input); 8 + assert(url.href === input, `expected href to preserve bracketed query, got ${url.href}`); 9 + assert(url.search === '?search[id]=14032,20025,26727', `unexpected search: ${url.search}`); 10 + assert( 11 + url.searchParams.get('search[id]') === '14032,20025,26727', 12 + `unexpected search param value: ${url.searchParams.get('search[id]')}` 13 + ); 14 + 15 + assert(URL.canParse(input) === true, 'expected URL.canParse to accept bracketed query URL'); 16 + 17 + const parsed = URL.parse(input); 18 + assert(parsed !== null, 'expected URL.parse to return a URL object'); 19 + assert(parsed.href === input, `expected URL.parse href to preserve bracketed query, got ${parsed.href}`); 20 + 21 + const request = new Request(input); 22 + assert(request.url === input, `expected Request.url to preserve bracketed query, got ${request.url}`); 23 + 24 + console.log('url bracket query test passed');