/src/serenity/Userland/Libraries/LibWeb/Fetch/FetchMethod.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Debug.h> |
8 | | #include <AK/TypeCasts.h> |
9 | | #include <LibJS/Runtime/PromiseCapability.h> |
10 | | #include <LibWeb/Bindings/ExceptionOrUtils.h> |
11 | | #include <LibWeb/Bindings/HostDefined.h> |
12 | | #include <LibWeb/DOM/AbortSignal.h> |
13 | | #include <LibWeb/Fetch/FetchMethod.h> |
14 | | #include <LibWeb/Fetch/Fetching/Fetching.h> |
15 | | #include <LibWeb/Fetch/Fetching/RefCountedFlag.h> |
16 | | #include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h> |
17 | | #include <LibWeb/Fetch/Infrastructure/FetchController.h> |
18 | | #include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h> |
19 | | #include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h> |
20 | | #include <LibWeb/Fetch/Request.h> |
21 | | #include <LibWeb/Fetch/Response.h> |
22 | | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> |
23 | | #include <LibWeb/WebIDL/ExceptionOr.h> |
24 | | #include <LibWeb/WebIDL/Promise.h> |
25 | | |
26 | | namespace Web::Fetch { |
27 | | |
28 | | // https://fetch.spec.whatwg.org/#dom-global-fetch |
29 | | JS::NonnullGCPtr<JS::Promise> fetch(JS::VM& vm, RequestInfo const& input, RequestInit const& init) |
30 | 0 | { |
31 | 0 | auto& realm = *vm.current_realm(); |
32 | | |
33 | | // 1. Let p be a new promise. |
34 | 0 | auto promise_capability = WebIDL::create_promise(realm); |
35 | | |
36 | | // 2. Let requestObject be the result of invoking the initial value of Request as constructor with input and init |
37 | | // as arguments. If this throws an exception, reject p with it and return p. |
38 | 0 | auto exception_or_request_object = Request::construct_impl(realm, input, init); |
39 | 0 | if (exception_or_request_object.is_exception()) { |
40 | 0 | auto throw_completion = Bindings::dom_exception_to_throw_completion(vm, exception_or_request_object.exception()); |
41 | 0 | WebIDL::reject_promise(realm, promise_capability, *throw_completion.value()); |
42 | 0 | return verify_cast<JS::Promise>(*promise_capability->promise().ptr()); |
43 | 0 | } |
44 | 0 | auto request_object = exception_or_request_object.release_value(); |
45 | | |
46 | | // 3. Let request be requestObject’s request. |
47 | 0 | auto request = request_object->request(); |
48 | | |
49 | | // 4. If requestObject’s signal is aborted, then: |
50 | 0 | if (request_object->signal()->aborted()) { |
51 | | // 1. Abort the fetch() call with p, request, null, and requestObject’s signal’s abort reason. |
52 | 0 | abort_fetch(realm, promise_capability, request, nullptr, request_object->signal()->reason()); |
53 | | |
54 | | // 2. Return p. |
55 | 0 | return verify_cast<JS::Promise>(*promise_capability->promise().ptr()); |
56 | 0 | } |
57 | | |
58 | | // 5. Let globalObject be request’s client’s global object. |
59 | 0 | auto& global_object = request->client()->global_object(); |
60 | | |
61 | | // FIXME: 6. If globalObject is a ServiceWorkerGlobalScope object, then set request’s service-workers mode to "none". |
62 | 0 | (void)global_object; |
63 | | |
64 | | // 7. Let responseObject be null. |
65 | 0 | JS::GCPtr<Response> response_object; |
66 | | |
67 | | // 8. Let relevantRealm be this’s relevant Realm. |
68 | | // NOTE: This assumes that the running execution context is for the fetch() function call. |
69 | 0 | auto& relevant_realm = HTML::relevant_realm(*vm.running_execution_context().function); |
70 | | |
71 | | // 9. Let locallyAborted be false. |
72 | | // NOTE: This lets us reject promises with predictable timing, when the request to abort comes from the same thread |
73 | | // as the call to fetch. |
74 | 0 | auto locally_aborted = Fetching::RefCountedFlag::create(false); |
75 | | |
76 | | // 10. Let controller be null. |
77 | 0 | JS::GCPtr<Infrastructure::FetchController> controller; |
78 | | |
79 | | // NOTE: Step 11 is done out of order so that the controller is non-null when we capture the GCPtr by copy in the abort algorithm lambda. |
80 | | // This is not observable, AFAICT. |
81 | | |
82 | | // 12. Set controller to the result of calling fetch given request and processResponse given response being these |
83 | | // steps: |
84 | 0 | auto process_response = [locally_aborted, promise_capability, request, response_object, &relevant_realm](JS::NonnullGCPtr<Infrastructure::Response> response) mutable { |
85 | | // 1. If locallyAborted is true, then abort these steps. |
86 | 0 | if (locally_aborted->value()) |
87 | 0 | return; |
88 | | |
89 | | // AD-HOC: An execution context is required for Promise functions. |
90 | 0 | HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(relevant_realm) }; |
91 | | |
92 | | // 2. If response’s aborted flag is set, then: |
93 | 0 | if (response->aborted()) { |
94 | | // FIXME: 1. Let deserializedError be the result of deserialize a serialized abort reason given controller’s |
95 | | // serialized abort reason and relevantRealm. |
96 | 0 | auto deserialized_error = JS::js_undefined(); |
97 | | |
98 | | // 2. Abort the fetch() call with p, request, responseObject, and deserializedError. |
99 | 0 | abort_fetch(relevant_realm, promise_capability, request, response_object, deserialized_error); |
100 | | |
101 | | // 3. Abort these steps. |
102 | 0 | return; |
103 | 0 | } |
104 | | |
105 | | // 3. If response is a network error, then reject p with a TypeError and abort these steps. |
106 | 0 | if (response->is_network_error()) { |
107 | 0 | auto message = response->network_error_message().value_or("Response is a network error"sv); |
108 | 0 | WebIDL::reject_promise(relevant_realm, promise_capability, JS::TypeError::create(relevant_realm, message)); |
109 | 0 | return; |
110 | 0 | } |
111 | | |
112 | | // 4. Set responseObject to the result of creating a Response object, given response, "immutable", and |
113 | | // relevantRealm. |
114 | 0 | response_object = Response::create(relevant_realm, response, Headers::Guard::Immutable); |
115 | | |
116 | | // 5. Resolve p with responseObject. |
117 | 0 | WebIDL::resolve_promise(relevant_realm, promise_capability, response_object); |
118 | 0 | }; |
119 | 0 | controller = MUST(Fetching::fetch( |
120 | 0 | realm, |
121 | 0 | request, |
122 | 0 | Infrastructure::FetchAlgorithms::create(vm, |
123 | 0 | { |
124 | 0 | .process_request_body_chunk_length = {}, |
125 | 0 | .process_request_end_of_body = {}, |
126 | 0 | .process_early_hints_response = {}, |
127 | 0 | .process_response = move(process_response), |
128 | 0 | .process_response_end_of_body = {}, |
129 | 0 | .process_response_consume_body = {}, |
130 | 0 | }))); |
131 | | |
132 | | // 11. Add the following abort steps to requestObject’s signal: |
133 | 0 | request_object->signal()->add_abort_algorithm([locally_aborted, request, controller, promise_capability, request_object, response_object, &relevant_realm] { |
134 | 0 | dbgln_if(WEB_FETCH_DEBUG, "Fetch: Request object signal's abort algorithm called"); |
135 | | |
136 | | // 1. Set locallyAborted to true. |
137 | 0 | locally_aborted->set_value(true); |
138 | | |
139 | | // 2. Assert: controller is non-null. |
140 | 0 | VERIFY(controller); |
141 | | |
142 | | // 3. Abort controller with requestObject’s signal’s abort reason. |
143 | 0 | controller->abort(relevant_realm, request_object->signal()->reason()); |
144 | | |
145 | | // AD-HOC: An execution context is required for Promise functions. |
146 | 0 | HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(relevant_realm) }; |
147 | | |
148 | | // 4. Abort the fetch() call with p, request, responseObject, and requestObject’s signal’s abort reason. |
149 | 0 | abort_fetch(relevant_realm, *promise_capability, request, response_object, request_object->signal()->reason()); |
150 | 0 | }); |
151 | | |
152 | | // 13. Return p. |
153 | 0 | return verify_cast<JS::Promise>(*promise_capability->promise().ptr()); |
154 | 0 | } |
155 | | |
156 | | // https://fetch.spec.whatwg.org/#abort-fetch |
157 | | void abort_fetch(JS::Realm& realm, WebIDL::Promise const& promise, JS::NonnullGCPtr<Infrastructure::Request> request, JS::GCPtr<Response> response_object, JS::Value error) |
158 | 0 | { |
159 | 0 | dbgln_if(WEB_FETCH_DEBUG, "Fetch: Aborting fetch with: request @ {}, error = {}", request.ptr(), error); |
160 | | |
161 | | // 1. Reject promise with error. |
162 | | // NOTE: This is a no-op if promise has already fulfilled. |
163 | 0 | WebIDL::reject_promise(realm, promise, error); |
164 | | |
165 | | // 2. If request’s body is non-null and is readable, then cancel request’s body with error. |
166 | 0 | if (auto* body = request->body().get_pointer<JS::NonnullGCPtr<Infrastructure::Body>>(); body != nullptr && (*body)->stream()->is_readable()) { |
167 | | // TODO: Implement cancelling streams |
168 | 0 | (void)error; |
169 | 0 | } |
170 | | |
171 | | // 3. If responseObject is null, then return. |
172 | 0 | if (response_object == nullptr) |
173 | 0 | return; |
174 | | |
175 | | // 4. Let response be responseObject’s response. |
176 | 0 | auto response = response_object->response(); |
177 | | |
178 | | // 5. If response’s body is non-null and is readable, then error response’s body with error. |
179 | 0 | if (response->body()) { |
180 | 0 | auto stream = response->body()->stream(); |
181 | 0 | if (stream->is_readable()) { |
182 | | // TODO: Implement erroring streams |
183 | 0 | (void)error; |
184 | 0 | } |
185 | 0 | } |
186 | 0 | } |
187 | | |
188 | | } |