blob: ee8e2aa9a7fecadc9fef46547bf6cef8c9b389bd [file] [log] [blame]
/*
* Copyright (C) 2016 Canon Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted, provided that the following conditions
* are required to be met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Canon Inc. nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "FetchResponse.h"
#include "FetchRequest.h"
#include "HTTPParsers.h"
#include "InspectorInstrumentation.h"
#include "JSBlob.h"
#include "MIMETypeRegistry.h"
#include "ReadableStreamSink.h"
#include "ResourceError.h"
#include "ScriptExecutionContext.h"
#include <wtf/text/StringConcatenateNumbers.h>
namespace WebCore {
// https://fetch.spec.whatwg.org/#null-body-status
static inline bool isNullBodyStatus(int status)
{
return status == 101 || status == 204 || status == 205 || status == 304;
}
Ref<FetchResponse> FetchResponse::create(ScriptExecutionContext& context, Optional<FetchBody>&& body, FetchHeaders::Guard guard, ResourceResponse&& response)
{
bool isSynthetic = response.type() == ResourceResponse::Type::Default || response.type() == ResourceResponse::Type::Error;
bool isOpaque = response.tainting() == ResourceResponse::Tainting::Opaque;
auto headers = isOpaque ? FetchHeaders::create(guard) : FetchHeaders::create(guard, HTTPHeaderMap { response.httpHeaderFields() });
auto fetchResponse = adoptRef(*new FetchResponse(context, WTFMove(body), WTFMove(headers), WTFMove(response)));
fetchResponse->updateContentType();
if (!isSynthetic)
fetchResponse->m_filteredResponse = ResourceResponseBase::filter(fetchResponse->m_internalResponse);
if (isOpaque)
fetchResponse->setBodyAsOpaque();
return fetchResponse;
}
ExceptionOr<Ref<FetchResponse>> FetchResponse::create(ScriptExecutionContext& context, Optional<FetchBody::Init>&& body, 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)
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.
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".
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.
if (init.headers) {
auto result = headers->fill(*init.headers);
if (result.hasException())
return result.releaseException();
}
Optional<FetchBody> extractedBody;
// 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.)
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();
// 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);
}
// 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), { }));
r->m_contentType = contentType;
auto mimeType = extractMIMETypeFromMediaType(contentType);
r->m_internalResponse.setMimeType(mimeType.isEmpty() ? defaultMIMEType() : mimeType);
r->m_internalResponse.setTextEncodingName(extractCharsetFromMediaType(contentType));
r->m_internalResponse.setHTTPStatusCode(status);
r->m_internalResponse.setHTTPStatusText(statusText);
return r;
}
Ref<FetchResponse> FetchResponse::error(ScriptExecutionContext& context)
{
auto response = adoptRef(*new FetchResponse(context, { }, FetchHeaders::create(FetchHeaders::Guard::Immutable), { }));
response->m_internalResponse.setType(Type::Error);
return response;
}
ExceptionOr<Ref<FetchResponse>> FetchResponse::redirect(ScriptExecutionContext& context, const String& url, int status)
{
// FIXME: Tighten the URL parsing algorithm according https://url.spec.whatwg.org/#concept-url-parser.
URL requestURL = context.completeURL(url);
if (!requestURL.isValid())
return Exception { TypeError, makeString("Redirection URL '", requestURL.string(), "' is invalid") };
if (!requestURL.user().isEmpty() || !requestURL.pass().isEmpty())
return Exception { TypeError, "Redirection URL contains credentials"_s };
if (!ResourceResponse::isRedirectionStatusCode(status))
return Exception { RangeError, makeString("Status code ", status, "is not a redirection status code") };
auto redirectResponse = adoptRef(*new FetchResponse(context, { }, FetchHeaders::create(FetchHeaders::Guard::Immutable), { }));
redirectResponse->m_internalResponse.setHTTPStatusCode(status);
redirectResponse->m_internalResponse.setHTTPHeaderField(HTTPHeaderName::Location, requestURL.string());
redirectResponse->m_headers->fastSet(HTTPHeaderName::Location, requestURL.string());
return redirectResponse;
}
FetchResponse::FetchResponse(ScriptExecutionContext& context, Optional<FetchBody>&& body, Ref<FetchHeaders>&& headers, ResourceResponse&& response)
: FetchBodyOwner(context, WTFMove(body), WTFMove(headers))
, m_internalResponse(WTFMove(response))
{
}
ExceptionOr<Ref<FetchResponse>> FetchResponse::clone(ScriptExecutionContext& context)
{
if (isDisturbedOrLocked())
return Exception { TypeError, "Body is disturbed or locked"_s };
ASSERT(scriptExecutionContext());
// If loading, let's create a stream so that data is teed on both clones.
if (isLoading() && !m_readableStreamSource)
createReadableStream(*context.execState());
// Synthetic responses do not store headers in m_internalResponse.
if (m_internalResponse.type() == ResourceResponse::Type::Default)
m_internalResponse.setHTTPHeaderFields(HTTPHeaderMap { headers().internalHeaders() });
auto clone = FetchResponse::create(context, WTF::nullopt, headers().guard(), ResourceResponse { m_internalResponse });
clone->cloneBody(*this);
clone->m_opaqueLoadIdentifier = m_opaqueLoadIdentifier;
clone->m_bodySizeWithPadding = m_bodySizeWithPadding;
return clone;
}
void FetchResponse::addAbortSteps(Ref<AbortSignal>&& signal)
{
m_abortSignal = WTFMove(signal);
m_abortSignal->addAlgorithm([this, weakThis = makeWeakPtr(this)] {
// FIXME: Cancel request body if it is a stream.
if (!weakThis)
return;
m_abortSignal = nullptr;
setLoadingError(Exception { AbortError, "Fetch is aborted"_s });
if (m_bodyLoader) {
if (auto callback = m_bodyLoader->takeNotificationCallback())
callback(Exception { AbortError, "Fetch is aborted"_s });
if (auto callback = m_bodyLoader->takeConsumeDataCallback())
callback(Exception { AbortError, "Fetch is aborted"_s });
}
if (m_readableStreamSource) {
if (!m_readableStreamSource->isCancelling())
m_readableStreamSource->error(*loadingException());
m_readableStreamSource = nullptr;
}
if (m_body)
m_body->loadingFailed(*loadingException());
if (auto bodyLoader = WTFMove(m_bodyLoader))
bodyLoader->stop();
});
}
void FetchResponse::fetch(ScriptExecutionContext& context, FetchRequest& request, NotificationCallback&& responseCallback)
{
if (request.signal().aborted()) {
responseCallback(Exception { AbortError, "Request signal is aborted"_s });
// FIXME: Cancel request body if it is a stream.
return;
}
if (request.hasReadableStreamBody()) {
responseCallback(Exception { NotSupportedError, "ReadableStream uploading is not supported"_s });
return;
}
InspectorInstrumentation::willFetch(context, request.url());
auto response = adoptRef(*new FetchResponse(context, FetchBody { }, FetchHeaders::create(FetchHeaders::Guard::Immutable), { }));
response->body().consumer().setAsLoading();
response->addAbortSteps(request.signal());
response->m_bodyLoader = makeUnique<BodyLoader>(response.get(), WTFMove(responseCallback));
if (!response->m_bodyLoader->start(context, request))
response->m_bodyLoader = nullptr;
}
const String& FetchResponse::url() const
{
if (m_responseURL.isNull()) {
URL url = filteredResponse().url();
url.removeFragmentIdentifier();
m_responseURL = url.string();
}
return m_responseURL;
}
const ResourceResponse& FetchResponse::filteredResponse() const
{
if (m_filteredResponse)
return m_filteredResponse.value();
return m_internalResponse;
}
void FetchResponse::BodyLoader::didSucceed()
{
ASSERT(m_response.hasPendingActivity());
m_response.m_body->loadingSucceeded();
#if ENABLE(STREAMS_API)
if (m_response.m_readableStreamSource) {
if (m_response.body().consumer().hasData())
m_response.m_readableStreamSource->enqueue(m_response.body().consumer().takeAsArrayBuffer());
m_response.closeStream();
}
#endif
if (auto consumeDataCallback = WTFMove(m_consumeDataCallback))
consumeDataCallback(nullptr);
if (m_loader->isStarted()) {
Ref<FetchResponse> protector(m_response);
m_response.m_bodyLoader = nullptr;
}
}
void FetchResponse::BodyLoader::didFail(const ResourceError& error)
{
ASSERT(m_response.hasPendingActivity());
m_response.setLoadingError(ResourceError { error });
if (auto responseCallback = WTFMove(m_responseCallback))
responseCallback(Exception { TypeError, error.localizedDescription() });
if (auto consumeDataCallback = WTFMove(m_consumeDataCallback))
consumeDataCallback(Exception { TypeError, error.localizedDescription() });
#if ENABLE(STREAMS_API)
if (m_response.m_readableStreamSource) {
if (!m_response.m_readableStreamSource->isCancelling())
m_response.m_readableStreamSource->error(*m_response.loadingException());
m_response.m_readableStreamSource = nullptr;
}
#endif
// Check whether didFail is called as part of FetchLoader::start.
if (m_loader && m_loader->isStarted()) {
Ref<FetchResponse> protector(m_response);
m_response.m_bodyLoader = nullptr;
}
}
FetchResponse::BodyLoader::BodyLoader(FetchResponse& response, NotificationCallback&& responseCallback)
: m_response(response)
, m_responseCallback(WTFMove(responseCallback))
, m_pendingActivity(m_response.makePendingActivity(m_response))
{
}
FetchResponse::BodyLoader::~BodyLoader()
{
}
static uint64_t nextOpaqueLoadIdentifier { 0 };
void FetchResponse::BodyLoader::didReceiveResponse(const ResourceResponse& resourceResponse)
{
m_response.m_filteredResponse = ResourceResponseBase::filter(resourceResponse);
m_response.m_internalResponse = resourceResponse;
m_response.m_internalResponse.setType(m_response.m_filteredResponse->type());
if (resourceResponse.tainting() == ResourceResponse::Tainting::Opaque) {
m_response.m_opaqueLoadIdentifier = ++nextOpaqueLoadIdentifier;
m_response.setBodyAsOpaque();
}
m_response.m_headers->filterAndFill(m_response.m_filteredResponse->httpHeaderFields(), FetchHeaders::Guard::Response);
m_response.updateContentType();
if (auto responseCallback = WTFMove(m_responseCallback))
responseCallback(m_response);
}
void FetchResponse::BodyLoader::didReceiveData(const char* data, size_t size)
{
#if ENABLE(STREAMS_API)
ASSERT(m_response.m_readableStreamSource || m_consumeDataCallback);
#else
ASSERT(m_consumeDataCallback);
#endif
if (m_consumeDataCallback) {
ReadableStreamChunk chunk { reinterpret_cast<const uint8_t*>(data), size };
m_consumeDataCallback(&chunk);
return;
}
#if ENABLE(STREAMS_API)
auto& source = *m_response.m_readableStreamSource;
if (!source.isPulling()) {
m_response.body().consumer().append(data, size);
return;
}
if (m_response.body().consumer().hasData() && !source.enqueue(m_response.body().consumer().takeAsArrayBuffer())) {
stop();
return;
}
if (!source.enqueue(ArrayBuffer::tryCreate(data, size))) {
stop();
return;
}
source.resolvePullPromise();
#else
UNUSED_PARAM(data);
UNUSED_PARAM(size);
#endif
}
bool FetchResponse::BodyLoader::start(ScriptExecutionContext& context, const FetchRequest& request)
{
m_loader = makeUnique<FetchLoader>(*this, &m_response.m_body->consumer());
m_loader->start(context, request);
return m_loader->isStarted();
}
void FetchResponse::BodyLoader::stop()
{
m_responseCallback = { };
if (m_loader)
m_loader->stop();
}
void FetchResponse::BodyLoader::consumeDataByChunk(ConsumeDataByChunkCallback&& consumeDataCallback)
{
ASSERT(!m_consumeDataCallback);
m_consumeDataCallback = WTFMove(consumeDataCallback);
auto data = m_loader->startStreaming();
if (!data)
return;
ReadableStreamChunk chunk { reinterpret_cast<const uint8_t*>(data->data()), data->size() };
m_consumeDataCallback(&chunk);
}
FetchResponse::ResponseData FetchResponse::consumeBody()
{
ASSERT(!isBodyReceivedByChunk());
if (isBodyNull())
return nullptr;
ASSERT(!m_isDisturbed);
m_isDisturbed = true;
return body().take();
}
void FetchResponse::consumeBodyReceivedByChunk(ConsumeDataByChunkCallback&& callback)
{
ASSERT(isBodyReceivedByChunk());
ASSERT(!isDisturbed());
m_isDisturbed = true;
if (hasReadableStreamBody()) {
m_body->consumer().extract(*m_body->readableStream(), WTFMove(callback));
return;
}
ASSERT(isLoading());
m_bodyLoader->consumeDataByChunk(WTFMove(callback));
}
void FetchResponse::setBodyData(ResponseData&& data, uint64_t bodySizeWithPadding)
{
m_bodySizeWithPadding = bodySizeWithPadding;
WTF::switchOn(data,
[this](Ref<FormData>& formData) {
if (isBodyNull())
setBody({ });
body().setAsFormData(WTFMove(formData));
},
[this](Ref<SharedBuffer>& buffer) {
if (isBodyNull())
setBody({ });
body().consumer().setData(WTFMove(buffer));
},
[](std::nullptr_t&) {
}
);
}
#if ENABLE(STREAMS_API)
void FetchResponse::consumeChunk(Ref<JSC::Uint8Array>&& chunk)
{
body().consumer().append(chunk->data(), chunk->byteLength());
}
void FetchResponse::consumeBodyAsStream()
{
ASSERT(m_readableStreamSource);
if (!isLoading()) {
FetchBodyOwner::consumeBodyAsStream();
return;
}
ASSERT(m_bodyLoader);
auto data = m_bodyLoader->startStreaming();
if (data) {
if (!m_readableStreamSource->enqueue(data->tryCreateArrayBuffer())) {
stop();
return;
}
m_readableStreamSource->resolvePullPromise();
}
}
void FetchResponse::closeStream()
{
ASSERT(m_readableStreamSource);
m_readableStreamSource->close();
m_readableStreamSource = nullptr;
}
void FetchResponse::feedStream()
{
ASSERT(m_readableStreamSource);
bool shouldCloseStream = !m_bodyLoader;
if (body().consumer().hasData()) {
if (!m_readableStreamSource->enqueue(body().consumer().takeAsArrayBuffer())) {
stop();
return;
}
if (!shouldCloseStream) {
m_readableStreamSource->resolvePullPromise();
return;
}
} else if (!shouldCloseStream)
return;
closeStream();
}
RefPtr<SharedBuffer> FetchResponse::BodyLoader::startStreaming()
{
ASSERT(m_loader);
return m_loader->startStreaming();
}
void FetchResponse::cancel()
{
m_isDisturbed = true;
stop();
}
#endif
void FetchResponse::stop()
{
RefPtr<FetchResponse> protectedThis(this);
FetchBodyOwner::stop();
if (auto bodyLoader = WTFMove(m_bodyLoader))
bodyLoader->stop();
}
const char* FetchResponse::activeDOMObjectName() const
{
return "Response";
}
bool FetchResponse::canSuspendForDocumentSuspension() const
{
// FIXME: We can probably do the same strategy as XHR.
return !isActive();
}
ResourceResponse FetchResponse::resourceResponse() const
{
auto response = m_internalResponse;
if (headers().guard() != FetchHeaders::Guard::Immutable) {
// FIXME: Add a setHTTPHeaderFields on ResourceResponseBase.
for (auto& header : headers().internalHeaders())
response.setHTTPHeaderField(header.key, header.value);
}
return response;
}
} // namespace WebCore