diff --git a/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any-expected.txt index 1b7e974d647c..da38a0a8b11a 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any-expected.txt @@ -1,19 +1,15 @@ -FAIL Check response returned by static json() with init undefined promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"status":400} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"statusText":"foo"} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{"x-foo":"bar"}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" +PASS Check response returned by static json() with init undefined +PASS Check response returned by static json() with init {"status":400} +PASS Check response returned by static json() with init {"statusText":"foo"} +PASS Check response returned by static json() with init {"headers":{}} +PASS Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}} +PASS Check response returned by static json() with init {"headers":{"x-foo":"bar"}} PASS Throws TypeError when calling static json() with a status of 204 PASS Throws TypeError when calling static json() with a status of 205 PASS Throws TypeError when calling static json() with a status of 304 -FAIL Check static json() encodes JSON objects correctly promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json({ foo: "bar" })', 'Response.json' is undefined)" +PASS Check static json() encodes JSON objects correctly PASS Check static json() throws when data is not encodable PASS Check static json() throws when data is circular -FAIL Check static json() propagates JSON serializer errors assert_throws_js: function "function () { - Response.json({ get foo() { throw new CustomError("bar") }}); - }" threw object "TypeError: Response.json is not a function. (In 'Response.json({ get foo() { throw new CustomError("bar") }})', 'Response.json' is undefined)" ("TypeError") expected instance of function "class CustomError extends Error { - name = "CustomError"; - }" ("CustomError") +PASS Check static json() propagates JSON serializer errors diff --git a/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.serviceworker-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.serviceworker-expected.txt index 1b7e974d647c..da38a0a8b11a 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.serviceworker-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.serviceworker-expected.txt @@ -1,19 +1,15 @@ -FAIL Check response returned by static json() with init undefined promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"status":400} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"statusText":"foo"} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{"x-foo":"bar"}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" +PASS Check response returned by static json() with init undefined +PASS Check response returned by static json() with init {"status":400} +PASS Check response returned by static json() with init {"statusText":"foo"} +PASS Check response returned by static json() with init {"headers":{}} +PASS Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}} +PASS Check response returned by static json() with init {"headers":{"x-foo":"bar"}} PASS Throws TypeError when calling static json() with a status of 204 PASS Throws TypeError when calling static json() with a status of 205 PASS Throws TypeError when calling static json() with a status of 304 -FAIL Check static json() encodes JSON objects correctly promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json({ foo: "bar" })', 'Response.json' is undefined)" +PASS Check static json() encodes JSON objects correctly PASS Check static json() throws when data is not encodable PASS Check static json() throws when data is circular -FAIL Check static json() propagates JSON serializer errors assert_throws_js: function "function () { - Response.json({ get foo() { throw new CustomError("bar") }}); - }" threw object "TypeError: Response.json is not a function. (In 'Response.json({ get foo() { throw new CustomError("bar") }})', 'Response.json' is undefined)" ("TypeError") expected instance of function "class CustomError extends Error { - name = "CustomError"; - }" ("CustomError") +PASS Check static json() propagates JSON serializer errors diff --git a/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.sharedworker-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.sharedworker-expected.txt index 1b7e974d647c..da38a0a8b11a 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.sharedworker-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.sharedworker-expected.txt @@ -1,19 +1,15 @@ -FAIL Check response returned by static json() with init undefined promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"status":400} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"statusText":"foo"} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{"x-foo":"bar"}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" +PASS Check response returned by static json() with init undefined +PASS Check response returned by static json() with init {"status":400} +PASS Check response returned by static json() with init {"statusText":"foo"} +PASS Check response returned by static json() with init {"headers":{}} +PASS Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}} +PASS Check response returned by static json() with init {"headers":{"x-foo":"bar"}} PASS Throws TypeError when calling static json() with a status of 204 PASS Throws TypeError when calling static json() with a status of 205 PASS Throws TypeError when calling static json() with a status of 304 -FAIL Check static json() encodes JSON objects correctly promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json({ foo: "bar" })', 'Response.json' is undefined)" +PASS Check static json() encodes JSON objects correctly PASS Check static json() throws when data is not encodable PASS Check static json() throws when data is circular -FAIL Check static json() propagates JSON serializer errors assert_throws_js: function "function () { - Response.json({ get foo() { throw new CustomError("bar") }}); - }" threw object "TypeError: Response.json is not a function. (In 'Response.json({ get foo() { throw new CustomError("bar") }})', 'Response.json' is undefined)" ("TypeError") expected instance of function "class CustomError extends Error { - name = "CustomError"; - }" ("CustomError") +PASS Check static json() propagates JSON serializer errors diff --git a/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.worker-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.worker-expected.txt index 1b7e974d647c..da38a0a8b11a 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.worker-expected.txt +++ b/LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-static-json.any.worker-expected.txt @@ -1,19 +1,15 @@ -FAIL Check response returned by static json() with init undefined promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"status":400} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"statusText":"foo"} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" -FAIL Check response returned by static json() with init {"headers":{"x-foo":"bar"}} promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json("hello world", init)', 'Response.json' is undefined)" +PASS Check response returned by static json() with init undefined +PASS Check response returned by static json() with init {"status":400} +PASS Check response returned by static json() with init {"statusText":"foo"} +PASS Check response returned by static json() with init {"headers":{}} +PASS Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}} +PASS Check response returned by static json() with init {"headers":{"x-foo":"bar"}} PASS Throws TypeError when calling static json() with a status of 204 PASS Throws TypeError when calling static json() with a status of 205 PASS Throws TypeError when calling static json() with a status of 304 -FAIL Check static json() encodes JSON objects correctly promise_test: Unhandled rejection with value: object "TypeError: Response.json is not a function. (In 'Response.json({ foo: "bar" })', 'Response.json' is undefined)" +PASS Check static json() encodes JSON objects correctly PASS Check static json() throws when data is not encodable PASS Check static json() throws when data is circular -FAIL Check static json() propagates JSON serializer errors assert_throws_js: function "function () { - Response.json({ get foo() { throw new CustomError("bar") }}); - }" threw object "TypeError: Response.json is not a function. (In 'Response.json({ get foo() { throw new CustomError("bar") }})', 'Response.json' is undefined)" ("TypeError") expected instance of function "class CustomError extends Error { - name = "CustomError"; - }" ("CustomError") +PASS Check static json() propagates JSON serializer errors diff --git a/Source/WebCore/Modules/fetch/FetchBody.h b/Source/WebCore/Modules/fetch/FetchBody.h index 7633f13a380b..fe78f7bb7704 100644 --- a/Source/WebCore/Modules/fetch/FetchBody.h +++ b/Source/WebCore/Modules/fetch/FetchBody.h @@ -60,6 +60,11 @@ class FetchBody { WEBCORE_EXPORT ~FetchBody(); FetchBody& operator=(FetchBody&&) = default; + explicit FetchBody(String&& data) + : m_data(WTFMove(data)) + { + } + WEBCORE_EXPORT static std::optional fromFormData(ScriptExecutionContext&, Ref&&); void loadingFailed(const Exception&); @@ -96,7 +101,6 @@ class FetchBody { explicit FetchBody(Ref&& data) : m_data(WTFMove(data)) { } explicit FetchBody(Ref&& data) : m_data(WTFMove(data)) { } explicit FetchBody(Ref&& data) : m_data(WTFMove(data)) { } - explicit FetchBody(String&& data) : m_data(WTFMove(data)) { } explicit FetchBody(Ref&& data) : m_data(WTFMove(data)) { } explicit FetchBody(Ref&& stream) : m_data(stream) { m_readableStream = WTFMove(stream); } explicit FetchBody(FetchBodyConsumer&& consumer) : m_consumer(WTFMove(consumer)) { } @@ -130,4 +134,9 @@ class FetchBody { RefPtr m_readableStream; }; +struct FetchBodyWithType { + FetchBody body; + String type; +}; + } // namespace WebCore diff --git a/Source/WebCore/Modules/fetch/FetchResponse.cpp b/Source/WebCore/Modules/fetch/FetchResponse.cpp index 68431d37181f..9ccd18ed8bdd 100644 --- a/Source/WebCore/Modules/fetch/FetchResponse.cpp +++ b/Source/WebCore/Modules/fetch/FetchResponse.cpp @@ -65,70 +65,49 @@ Ref FetchResponse::create(ScriptExecutionContext* context, std::o return fetchResponse; } -ExceptionOr> FetchResponse::create(ScriptExecutionContext& context, std::optional&& body, Init&& init) +ExceptionOr> FetchResponse::create(ScriptExecutionContext& context, std::optional&& bodyWithType, Init&& init) { - // 1. If init’s status member is not in the range 200 to 599, inclusive, then throw a RangeError. - if (init.status < 200 || init.status > 599) + // https://fetch.spec.whatwg.org/#initialize-a-response + // 1. If init["status"] is not in the range 200 to 599, inclusive, then throw a RangeError. + if (init.status < 200 || init.status > 599) return Exception { RangeError, "Status must be between 200 and 599"_s }; - // 2. If init’s statusText member does not match the reason-phrase token production, then throw a TypeError. + // 2. If init["statusText"] does not match the reason-phrase token production, then throw a TypeError. if (!isValidReasonPhrase(init.statusText)) return Exception { TypeError, "Status text must be a valid reason-phrase."_s }; - // 3. Let r be a new Response object associated with a new response. - // NOTE: Creation of the Response object is delayed until all potential exceptional cases are handled. - - // 4. Set r’s headers to a new Headers object, whose header list is r’s response’s header list, and guard is "response". + // Both uses of "initialize a response" (the Response constructor and Response.json) create the + // Response object with the "response" header guard. auto headers = FetchHeaders::create(FetchHeaders::Guard::Response); - // 5. Set r’s response’s status to init’s status member. - auto status = init.status; - - // 6. Set r’s response’s status message to init’s statusText member. - auto statusText = init.statusText; - - // 7. If init’s headers member is present, then fill r’s headers with init’s headers member. + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. if (init.headers) { auto result = headers->fill(*init.headers); if (result.hasException()) return result.releaseException(); } - std::optional extractedBody; + std::optional body; - // 8. If body is non-null, run these substeps: - if (body) { - // 8.1 If init’s status member is a null body status, then throw a TypeError. - // (NOTE: 101 is included in null body status due to its use elsewhere. It does not affect this step.) + // 6. If body was given, then: + if (bodyWithType) { + // 6.1 If response’s status is a null body status, then throw a TypeError. + // (NOTE: 101 and 103 are included in null body status due to their use elsewhere. It does not affect this step.) if (isNullBodyStatus(init.status)) return Exception { TypeError, "Response cannot have a body with the given status."_s }; - // 8.2 Let Content-Type be null. - String contentType; - - // 8.3 Set r’s response’s body and Content-Type to the result of extracting body. - auto result = FetchBody::extract(WTFMove(*body), contentType); - if (result.hasException()) - return result.releaseException(); - extractedBody = result.releaseReturnValue(); + // 6.2 Set response’s body to body’s body. + body = WTFMove(bodyWithType->body); - // 8.4 If Content-Type is non-null and r’s response’s header list does not contain `Content-Type`, then append - // `Content-Type`/Content-Type to r’s response’s header list. - if (!contentType.isNull() && !headers->fastHas(HTTPHeaderName::ContentType)) - headers->fastSet(HTTPHeaderName::ContentType, contentType); + // 6.3 If body’s type is non-null and response’s header list does not contain `Content-Type`, then append + // (`Content-Type`, body’s type) to response’s header list. + if (!bodyWithType->type.isNull() && !headers->fastHas(HTTPHeaderName::ContentType)) + headers->fastSet(HTTPHeaderName::ContentType, bodyWithType->type); } - // 9. Set r’s MIME type to the result of extracting a MIME type from r’s response’s header list. auto contentType = headers->fastGet(HTTPHeaderName::ContentType); - // 10. Set r’s response’s HTTPS state to current settings object’s HTTPS state. - // FIXME: Implement. - - // 11. Resolve r’s trailer promise with a new Headers object whose guard is "immutable". - // FIXME: Implement. - - // 12. Return r. - auto r = adoptRef(*new FetchResponse(&context, WTFMove(extractedBody), WTFMove(headers), { })); + auto r = adoptRef(*new FetchResponse(&context, WTFMove(body), WTFMove(headers), { })); r->suspendIfNeeded(); r->m_contentType = contentType; @@ -136,12 +115,28 @@ ExceptionOr> FetchResponse::create(ScriptExecutionContext& co r->m_internalResponse.setMimeType(mimeType.isEmpty() ? AtomString { defaultMIMEType() } : mimeType); r->m_internalResponse.setTextEncodingName(extractCharsetFromMediaType(contentType).toAtomString()); - r->m_internalResponse.setHTTPStatusCode(status); - r->m_internalResponse.setHTTPStatusText(statusText); + // 3. Set response’s response’s status to init["status"]. + r->m_internalResponse.setHTTPStatusCode(init.status); + // 4. Set response’s response’s status message to init["statusText"]. + r->m_internalResponse.setHTTPStatusText(init.statusText); return r; } +ExceptionOr> FetchResponse::create(ScriptExecutionContext& context, std::optional&& body, Init&& init) +{ + std::optional bodyWithType; + if (body) { + String type; + auto result = FetchBody::extract(WTFMove(*body), type); + if (result.hasException()) + return result.releaseException(); + bodyWithType = { result.releaseReturnValue(), WTFMove(type) }; + } + + return FetchResponse::create(context, WTFMove(bodyWithType), WTFMove(init)); +} + Ref FetchResponse::error(ScriptExecutionContext& context) { auto response = adoptRef(*new FetchResponse(&context, { }, FetchHeaders::create(FetchHeaders::Guard::Immutable), { })); @@ -168,6 +163,16 @@ ExceptionOr> FetchResponse::redirect(ScriptExecutionContext& return redirectResponse; } +ExceptionOr> FetchResponse::jsonForBindings(ScriptExecutionContext& context, JSC::JSValue data, Init&& init) +{ + String jsonString = JSC::JSONStringify(context.globalObject(), data, 0); + if (jsonString.isNull()) + return Exception { TypeError, "Value doesn't have a JSON representation"_s }; + + FetchBodyWithType body { FetchBody(WTFMove(jsonString)), "application/json"_s }; + return FetchResponse::create(context, WTFMove(body), WTFMove(init)); +} + FetchResponse::FetchResponse(ScriptExecutionContext* context, std::optional&& body, Ref&& headers, ResourceResponse&& response) : FetchBodyOwner(context, WTFMove(body), WTFMove(headers)) , m_internalResponse(WTFMove(response)) diff --git a/Source/WebCore/Modules/fetch/FetchResponse.h b/Source/WebCore/Modules/fetch/FetchResponse.h index e25b2e225f76..ccd0266ae22c 100644 --- a/Source/WebCore/Modules/fetch/FetchResponse.h +++ b/Source/WebCore/Modules/fetch/FetchResponse.h @@ -61,8 +61,10 @@ class FetchResponse final : public FetchBodyOwner { WEBCORE_EXPORT static Ref create(ScriptExecutionContext*, std::optional&&, FetchHeaders::Guard, ResourceResponse&&); static ExceptionOr> create(ScriptExecutionContext&, std::optional&&, Init&&); + static ExceptionOr> create(ScriptExecutionContext&, std::optional&&, Init&&); static Ref error(ScriptExecutionContext&); static ExceptionOr> redirect(ScriptExecutionContext&, const String& url, int status); + static ExceptionOr> jsonForBindings(ScriptExecutionContext&, JSC::JSValue data, Init&&); using NotificationCallback = Function>&&)>; static void fetch(ScriptExecutionContext&, FetchRequest&, NotificationCallback&&, const String& initiator); diff --git a/Source/WebCore/Modules/fetch/FetchResponse.idl b/Source/WebCore/Modules/fetch/FetchResponse.idl index c41139ae6d33..3960c99dd4a9 100644 --- a/Source/WebCore/Modules/fetch/FetchResponse.idl +++ b/Source/WebCore/Modules/fetch/FetchResponse.idl @@ -48,6 +48,7 @@ dictionary FetchResponseInit { [CallWith=CurrentScriptExecutionContext, NewObject] static FetchResponse error(); [CallWith=CurrentScriptExecutionContext, NewObject] static FetchResponse redirect(USVString url, optional unsigned short status = 302); + [CallWith=CurrentScriptExecutionContext, NewObject, ImplementedAs=jsonForBindings] static FetchResponse json(any data, optional FetchResponseInit init); readonly attribute FetchResponseType type;