diff --git a/examples/spec/fetch.js b/examples/spec/fetch.js new file mode 100644 index 00000000..6c7d2542 --- /dev/null +++ b/examples/spec/fetch.js @@ -0,0 +1,49 @@ +import { test, summary } from './helpers.js'; + +console.log('Fetch Tests\n'); + +async function testRejects(name, fn) { + try { + await fn(); + test(`${name} rejects`, false, true); + } catch (_err) { + test(`${name} rejects`, true, true); + } +} + +test('fetch typeof', typeof fetch, 'function'); + +const textResponse = await fetch('data:,hello%20world'); +test('fetch data url returns Response', textResponse instanceof Response, true); +test('fetch data url status', textResponse.status, 200); +test('fetch data url ok', textResponse.ok, true); +test('fetch data url url', textResponse.url, 'data:,hello%20world'); +test('fetch data url text', await textResponse.text(), 'hello world'); + +const jsonResponse = await fetch('data:application/json,%7B%22ok%22%3Atrue%7D'); +test('fetch data url json content-type', jsonResponse.headers.get('content-type'), 'application/json'); +test('fetch data url json body', (await jsonResponse.json()).ok, true); +test('fetch response headers immutable', (() => { + try { + jsonResponse.headers.append('x-test', '1'); + return false; + } catch (_err) { + return true; + } +})(), true); + +const requestInput = new Request('data:text/plain,request-body'); +const requestResponse = await fetch(requestInput); +test('fetch accepts Request input', await requestResponse.text(), 'request-body'); + +const abortedController = new AbortController(); +abortedController.abort(new Error('aborted')); +await testRejects('fetch with aborted signal', () => + fetch('data:,ignored', { signal: abortedController.signal }) +); + +await testRejects('fetch unsupported scheme', () => + fetch('file:///tmp/ant-fetch-unsupported') +); + +summary(); diff --git a/include/common.h b/include/common.h index 6a3ea9d0..7dc241dd 100644 --- a/include/common.h +++ b/include/common.h @@ -78,6 +78,7 @@ typedef enum { SLOT_REQUEST_BODY_STREAM, SLOT_RESPONSE_HEADERS, SLOT_RESPONSE_BODY_STREAM, + SLOT_PIPE_ABORT_LISTENER, SLOT_MAX = 255 } internal_slot_t; diff --git a/include/modules/abort.h b/include/modules/abort.h index 703d8885..0c5ddcdc 100644 --- a/include/modules/abort.h +++ b/include/modules/abort.h @@ -9,6 +9,7 @@ void init_abort_module(void); void gc_mark_abort(ant_t *js, gc_mark_fn mark); void signal_do_abort(ant_t *js, ant_value_t signal, ant_value_t reason); void abort_signal_add_listener(ant_t *js, ant_value_t signal, ant_value_t callback); +void abort_signal_remove_listener(ant_t *js, ant_value_t signal, ant_value_t callback); bool abort_signal_is_signal(ant_value_t signal); bool abort_signal_is_aborted(ant_value_t signal); diff --git a/include/modules/headers.h b/include/modules/headers.h index d68eba23..4874ab4a 100644 --- a/include/modules/headers.h +++ b/include/modules/headers.h @@ -15,12 +15,17 @@ typedef enum { HEADERS_GUARD_IMMUTABLE } headers_guard_t; -headers_guard_t headers_get_guard(ant_value_t hdrs); +typedef void (*headers_foreach_cb)( + const char *name, + const char *value, + void *ctx +); void init_headers_module(void); void headers_apply_guard(ant_value_t hdrs); void headers_set_guard(ant_value_t hdrs, headers_guard_t guard); void headers_append_if_missing(ant_value_t hdrs, const char *name, const char *value); +void headers_for_each(ant_value_t hdrs, headers_foreach_cb cb, void *ctx); bool headers_is_headers(ant_value_t obj); bool headers_copy_from(ant_t *js, ant_value_t dst, ant_value_t src); @@ -33,4 +38,6 @@ ant_value_t headers_create_from_init(ant_t *js, ant_value_t init); ant_value_t headers_get_value(ant_t *js, ant_value_t hdrs, const char *name); ant_value_t headers_append_value(ant_t *js, ant_value_t hdrs, ant_value_t name_v, ant_value_t value_v); +headers_guard_t headers_get_guard(ant_value_t hdrs); + #endif diff --git a/include/modules/http.h b/include/modules/http.h new file mode 100644 index 00000000..326912cd --- /dev/null +++ b/include/modules/http.h @@ -0,0 +1,63 @@ +#ifndef ANT_HTTP_H +#define ANT_HTTP_H + +#include +#include +#include +#include + +typedef struct ant_http_header_s { + char *name; + char *value; + struct ant_http_header_s *next; +} ant_http_header_t; + +typedef struct { + const char *method; + const char *url; + const ant_http_header_t *headers; + const uint8_t *body; + size_t body_len; +} ant_http_request_options_t; + +typedef struct { + int status; + const char *status_text; + const ant_http_header_t *headers; +} ant_http_response_t; + +typedef void (*ant_http_response_cb)( + ant_http_request_t *req, + const ant_http_response_t *resp, + void *user_data +); + +typedef void (*ant_http_body_cb)( + ant_http_request_t *req, + const uint8_t *chunk, + size_t len, + void *user_data +); + +typedef void (*ant_http_complete_cb)( + ant_http_request_t *req, + int error_code, + const char *error_message, + void *user_data +); + +int ant_http_request_start( + uv_loop_t *loop, + const ant_http_request_options_t *options, + ant_http_response_cb on_response, + ant_http_body_cb on_body, + ant_http_complete_cb on_complete, + void *user_data, + ant_http_request_t **out_req +); + +int ant_http_request_cancel(ant_http_request_t *req); +void ant_http_headers_free(ant_http_header_t *headers); +const ant_http_response_t *ant_http_request_response(ant_http_request_t *req); + +#endif diff --git a/include/modules/request.h b/include/modules/request.h index d7466c0e..38b9c966 100644 --- a/include/modules/request.h +++ b/include/modules/request.h @@ -45,4 +45,10 @@ ant_value_t request_create( const char *body_type ); +ant_value_t request_create_from_input_init( + ant_t *js, + ant_value_t input, + ant_value_t init +); + #endif diff --git a/include/modules/response.h b/include/modules/response.h index f57408cc..47431336 100644 --- a/include/modules/response.h +++ b/include/modules/response.h @@ -24,7 +24,6 @@ typedef struct { } response_data_t; extern ant_value_t g_response_proto; - void init_response_module(void); response_data_t *response_get_data(ant_value_t obj); @@ -42,4 +41,16 @@ ant_value_t response_create( headers_guard_t guard ); +ant_value_t response_create_fetched( + ant_t *js, + int status, + const char *status_text, + const char *url, + ant_value_t headers_obj, + const uint8_t *body, + size_t body_len, + ant_value_t body_stream, + const char *body_type +); + #endif diff --git a/include/types.h b/include/types.h index 86718e4b..7fba424e 100644 --- a/include/types.h +++ b/include/types.h @@ -10,8 +10,9 @@ struct ant_object; struct ant_shape; struct ant_isolate_t; -typedef struct ant_isolate_t ant_t; -typedef struct ant_pool_block ant_pool_block_t; +typedef struct ant_isolate_t ant_t; +typedef struct ant_pool_block ant_pool_block_t; +typedef struct ant_http_request_s ant_http_request_t; typedef struct ant_object ant_object_t; typedef struct ant_shape ant_shape_t; diff --git a/src/modules/abort.c b/src/modules/abort.c index 60a01a21..f69d9407 100644 --- a/src/modules/abort.c +++ b/src/modules/abort.c @@ -130,6 +130,19 @@ void signal_do_abort(ant_t *js, ant_value_t signal_obj, ant_value_t reason) { utarray_free(to_fire); } +void abort_signal_remove_listener(ant_t *js, ant_value_t signal, ant_value_t callback) { + abort_signal_data_t *data = get_signal_data(signal); + if (!data) return; + + unsigned int n = utarray_len(data->listeners); + for (unsigned int i = 0; i < n; i++) { + abort_listener_t *entry = (abort_listener_t *)utarray_eltptr(data->listeners, i); + if (entry->callback != callback) continue; + utarray_erase(data->listeners, i, 1); + return; + } +} + static ant_value_t make_new_signal(ant_t *js) { abort_signal_data_t *data = ant_calloc(sizeof(abort_signal_data_t)); if (!data) return js_mkerr(js, "AbortSignal: out of memory"); diff --git a/src/modules/headers.c b/src/modules/headers.c index 923935aa..39bfd22c 100644 --- a/src/modules/headers.c +++ b/src/modules/headers.c @@ -793,6 +793,12 @@ void headers_append_if_missing(ant_value_t hdrs, const char *name, const char *v free(lower); } +void headers_for_each(ant_value_t hdrs, headers_foreach_cb cb, void *ctx) { + hdr_list_t *l = get_list(hdrs); + if (!l || !cb) return; + for (hdr_entry_t *e = l->head; e; e = e->next) cb(e->name, e->value, ctx); +} + bool headers_set_literal(ant_t *js, ant_value_t hdrs, const char *name, const char *value) { hdr_list_t *l = get_list(hdrs); headers_guard_t guard = 0; diff --git a/src/modules/request.c b/src/modules/request.c index 3c2ef4e1..d6589c1b 100644 --- a/src/modules/request.c +++ b/src/modules/request.c @@ -1094,21 +1094,30 @@ static ant_value_t request_apply_ctor_body( } static ant_value_t js_request_ctor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t init = (nargs >= 2 && vtype(args[1]) != T_UNDEF) ? args[1] : js_mkundef(); + if (vtype(js->new_target) == T_UNDEF) return js_mkerr_typed(js, JS_ERR_TYPE, "Request constructor requires 'new'"); if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "Request constructor requires at least 1 argument"); ant_value_t input = args[0]; - ant_value_t init = (nargs >= 2 && vtype(args[1]) != T_UNDEF) ? args[1] : js_mkundef(); - bool init_provided = (vtype(init) == T_OBJ || vtype(init) == T_ARR); + ant_value_t obj = 0; + ant_value_t proto = 0; + bool init_provided = false; request_data_t *req = NULL; request_data_t *src = NULL; ant_value_t input_signal = js_mkundef(); - ant_value_t step = request_new_from_input(js, input, &req, &src, &input_signal); + ant_value_t step = js_mkundef(); + ant_value_t signal = 0; + ant_value_t headers = 0; + bool duplex_provided = false; + init_provided = (vtype(init) == T_OBJ || vtype(init) == T_ARR); + + step = request_new_from_input(js, input, &req, &src, &input_signal); if (is_err(step)) return step; if (init_provided) { @@ -1125,21 +1134,99 @@ static ant_value_t js_request_ctor(ant_t *js, ant_value_t *args, int nargs) { "Failed to construct 'Request': method must be one of GET, HEAD, POST for no-cors mode"); } - ant_value_t obj = js_mkobj(js); - ant_value_t proto = js_instance_proto_from_new_target(js, g_request_proto); + obj = js_mkobj(js); + proto = js_instance_proto_from_new_target(js, g_request_proto); if (is_object_type(proto)) js_set_proto_init(obj, proto); + else js_set_proto_init(obj, g_request_proto); + js_set_slot(obj, SLOT_BRAND, js_mknum(BRAND_REQUEST)); js_set_slot(obj, SLOT_DATA, ANT_PTR(req)); - ant_value_t signal = abort_signal_create_dependent(js, input_signal); + signal = abort_signal_create_dependent(js, input_signal); if (is_err(signal)) { data_free(req); return signal; } js_set_slot_wb(js, obj, SLOT_REQUEST_SIGNAL, signal); - ant_value_t headers = request_create_ctor_headers(js, input); - bool duplex_provided = false; + headers = request_create_ctor_headers(js, input); + if (is_err(headers)) { data_free(req); return headers; } + + if (init_provided) { + headers = request_apply_init_headers(js, init, headers); + if (is_err(headers)) { data_free(req); return headers; } + } + + headers_set_guard(headers, + strcmp(req->mode, "no-cors") == 0 + ? HEADERS_GUARD_REQUEST_NO_CORS + : HEADERS_GUARD_REQUEST + ); + + headers_apply_guard(headers); + js_set_slot_wb(js, obj, SLOT_REQUEST_HEADERS, headers); + + if (init_provided) { + step = request_parse_duplex(js, init, &duplex_provided); + if (is_err(step)) { data_free(req); return step; } + } + + step = request_apply_ctor_body( + js, obj, input, init, init_provided, + duplex_provided, req, src, headers + ); + + if (is_err(step)) { + data_free(req); + return step; + } + + if (src && src->has_body && !src->body_used) + src->body_used = true; + + return obj; +} + +ant_value_t request_create_from_input_init(ant_t *js, ant_value_t input, ant_value_t init) { + bool init_provided = (vtype(init) == T_OBJ || vtype(init) == T_ARR); + + request_data_t *req = NULL; + request_data_t *src = NULL; + + ant_value_t input_signal = js_mkundef(); + ant_value_t step = js_mkundef(); + ant_value_t obj = 0; + ant_value_t signal = 0; + ant_value_t headers = 0; + bool duplex_provided = false; + step = request_new_from_input(js, input, &req, &src, &input_signal); + if (is_err(step)) return step; + + if (init_provided) { + step = request_apply_init_options(js, init, req, &input_signal); + if (is_err(step)) { data_free(req); return step; } + } + + if ( + strcmp(req->mode, "no-cors") == 0 && + !is_cors_safelisted_method(req->method) + ) { + data_free(req); + return js_mkerr_typed(js, JS_ERR_TYPE, + "Failed to construct 'Request': method must be one of GET, HEAD, POST for no-cors mode"); + } + + obj = js_mkobj(js); + js_set_proto_init(obj, g_request_proto); + js_set_slot(obj, SLOT_BRAND, js_mknum(BRAND_REQUEST)); + js_set_slot(obj, SLOT_DATA, ANT_PTR(req)); + + signal = abort_signal_create_dependent(js, input_signal); + if (is_err(signal)) { data_free(req); return signal; } + js_set_slot_wb(js, obj, SLOT_REQUEST_SIGNAL, signal); + + headers = request_create_ctor_headers(js, input); if (is_err(headers)) { data_free(req); return headers; } + if (init_provided) { headers = request_apply_init_headers(js, init, headers); if (is_err(headers)) { data_free(req); return headers; } diff --git a/src/modules/response.c b/src/modules/response.c index 5e5d570c..36d61128 100644 --- a/src/modules/response.c +++ b/src/modules/response.c @@ -1044,6 +1044,77 @@ ant_value_t response_create( return obj; } +ant_value_t response_create_fetched( + ant_t *js, + int status, + const char *status_text, + const char *url, + ant_value_t headers_obj, + const uint8_t *body, + size_t body_len, + ant_value_t body_stream, + const char *body_type +) { + ant_value_t obj = response_new(HEADERS_GUARD_IMMUTABLE); + ant_value_t headers = 0; + response_data_t *resp = NULL; + url_state_t parsed = {0}; + + if (is_err(obj)) return obj; + resp = get_data(obj); + + resp->status = status; + free(resp->status_text); + resp->status_text = strdup(status_text ? status_text : ""); + if (!resp->status_text) { + data_free(resp); + return js_mkerr(js, "out of memory"); + } + + if (url && parse_url_to_state(url, NULL, &parsed) == 0) { + url_state_clear(&resp->url); + resp->url = parsed; + resp->has_url = true; + resp->url_list_size = 1; + } else url_state_clear(&parsed); + + if (rs_is_stream(body_stream)) { + resp->body_is_stream = true; + resp->has_body = true; + js_set_slot_wb(js, obj, SLOT_RESPONSE_BODY_STREAM, body_stream); + } else { + if (body_len > 0) { + resp->body_data = malloc(body_len); + if (!resp->body_data) { + data_free(resp); + return js_mkerr(js, "out of memory"); + } + memcpy(resp->body_data, body, body_len); + } + resp->body_size = body_len; + resp->body_is_stream = false; + resp->has_body = body || body_len > 0; + } + + resp->body_type = body_type ? strdup(body_type) : NULL; + if (body_type && !resp->body_type) { + data_free(resp); + return js_mkerr(js, "out of memory"); + } + + headers = is_object_type(headers_obj) ? headers_obj : headers_create_empty(js); + if (is_err(headers)) { + data_free(resp); + return headers; + } + + headers_set_guard(headers, HEADERS_GUARD_IMMUTABLE); + headers_apply_guard(headers); + js_set_slot_wb(js, obj, SLOT_RESPONSE_HEADERS, headers); + + return obj; +} + void init_response_module(void) { ant_t *js = rt->js; ant_value_t g = js_glob(js); diff --git a/src/modules/url.c b/src/modules/url.c index 624772fe..454762c4 100644 --- a/src/modules/url.c +++ b/src/modules/url.c @@ -316,18 +316,35 @@ int parse_url_to_state(const char *url_str, const char *base_str, url_state_t *s } char *build_href(const url_state_t *s) { - size_t len = strlen(s->protocol) + 2; - len += strlen(s->hostname) + strlen(s->pathname) + strlen(s->search) + strlen(s->hash) + 32; - + bool has_authority = + (s->hostname && *s->hostname) || + (s->username && *s->username) || + (s->password && *s->password) || + (s->port && *s->port); + + bool opaque_like = !has_authority && !is_special_scheme(s->protocol); + const char *pathname = s->pathname ? s->pathname : ""; + size_t len = strlen(s->protocol) + strlen(pathname) + strlen(s->search) + strlen(s->hash) + 32; + + if (has_authority) len += strlen(s->hostname) + 2; if (s->username && *s->username) len += strlen(s->username) + 1; if (s->password && *s->password) len += strlen(s->password) + 1; if (s->port && *s->port) len += strlen(s->port) + 1; char *href = malloc(len); if (!href) return strdup(""); + size_t pos = 0; - pos += (size_t)sprintf(href + pos, "%s//", s->protocol); + pos += (size_t)sprintf(href + pos, "%s", s->protocol); + if (opaque_like) { + if (pathname[0] == '/') pathname++; + pos += (size_t)sprintf(href + pos, "%s%s%s", pathname, s->search, s->hash); + href[pos] = '\0'; + return href; + } + + pos += (size_t)sprintf(href + pos, "//"); if (s->username && *s->username) { pos += (size_t)sprintf(href + pos, "%s", s->username); if (s->password && *s->password) @@ -338,10 +355,9 @@ char *build_href(const url_state_t *s) { pos += (size_t)sprintf(href + pos, "%s", s->hostname); if (s->port && *s->port) pos += (size_t)sprintf(href + pos, ":%s", s->port); - - pos += (size_t)sprintf(href + pos, "%s%s%s", s->pathname, s->search, s->hash); + + pos += (size_t)sprintf(href + pos, "%s%s%s", pathname, s->search, s->hash); href[pos] = '\0'; - return href; } diff --git a/src/streams/pipes.c b/src/streams/pipes.c index 59e40d34..0d312b5e 100644 --- a/src/streams/pipes.c +++ b/src/streams/pipes.c @@ -414,6 +414,7 @@ ant_value_t readable_stream_pipe_to( if (abort_signal_is_signal(signal)) { ant_value_t listener = js_heavy_mkfun(js, pipe_abort_listener, state); + js_set_slot(state, SLOT_PIPE_ABORT_LISTENER, listener); abort_signal_add_listener(js, signal, listener); } diff --git a/src/types/fetch.d.ts b/src/types/fetch.d.ts index 4f4c12d6..cdeef85e 100644 --- a/src/types/fetch.d.ts +++ b/src/types/fetch.d.ts @@ -1,14 +1 @@ -interface FetchResponse { - status: number; - ok: boolean; - body: string; - json(): unknown; -} - -interface FetchOptions { - method?: string; - headers?: Record; - body?: string; -} - -declare function fetch(url: string, options?: FetchOptions): Promise; +declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise;