mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-08 10:44:56 +00:00
Bug 1784880 - Support range requests on blob URLs in fetch/XMLHttpRequest; r=dlrobertson,necko-reviewers,jesup
Differential Revision: https://phabricator.services.mozilla.com/D184281
This commit is contained in:
parent
5361e039c9
commit
44b6f2153c
@ -7521,8 +7521,8 @@ Maybe<nsContentUtils::ParsedRange> nsContentUtils::ParseSingleRangeRequest(
|
||||
const nsACString& aHeaderValue, bool aAllowWhitespace) {
|
||||
// See https://fetch.spec.whatwg.org/#simple-range-header-value
|
||||
mozilla::Tokenizer p(aHeaderValue);
|
||||
Maybe<uint32_t> rangeStart;
|
||||
Maybe<uint32_t> rangeEnd;
|
||||
Maybe<uint64_t> rangeStart;
|
||||
Maybe<uint64_t> rangeEnd;
|
||||
|
||||
// Step 2 and 3
|
||||
if (!p.CheckWord("bytes")) {
|
||||
@ -7545,7 +7545,7 @@ Maybe<nsContentUtils::ParsedRange> nsContentUtils::ParseSingleRangeRequest(
|
||||
}
|
||||
|
||||
// Step 8 and 9
|
||||
int32_t res;
|
||||
uint64_t res;
|
||||
if (p.ReadInteger(&res)) {
|
||||
rangeStart = Some(res);
|
||||
}
|
||||
|
@ -2761,20 +2761,20 @@ class nsContentUtils {
|
||||
|
||||
class ParsedRange {
|
||||
public:
|
||||
explicit ParsedRange(mozilla::Maybe<uint32_t> aStart,
|
||||
mozilla::Maybe<uint32_t> aEnd)
|
||||
explicit ParsedRange(mozilla::Maybe<uint64_t> aStart,
|
||||
mozilla::Maybe<uint64_t> aEnd)
|
||||
: mStart(aStart), mEnd(aEnd) {}
|
||||
|
||||
mozilla::Maybe<uint32_t> Start() const { return mStart; }
|
||||
mozilla::Maybe<uint32_t> End() const { return mEnd; }
|
||||
mozilla::Maybe<uint64_t> Start() const { return mStart; }
|
||||
mozilla::Maybe<uint64_t> End() const { return mEnd; }
|
||||
|
||||
bool operator==(const ParsedRange& aOther) const {
|
||||
return Start() == aOther.Start() && End() == aOther.End();
|
||||
}
|
||||
|
||||
private:
|
||||
mozilla::Maybe<uint32_t> mStart;
|
||||
mozilla::Maybe<uint32_t> mEnd;
|
||||
mozilla::Maybe<uint64_t> mStart;
|
||||
mozilla::Maybe<uint64_t> mEnd;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include "nsIPipe.h"
|
||||
#include "nsIRedirectHistoryEntry.h"
|
||||
|
||||
#include "nsBaseChannel.h"
|
||||
#include "nsContentPolicyUtils.h"
|
||||
#include "nsDataHandler.h"
|
||||
#include "nsNetUtil.h"
|
||||
@ -843,6 +844,21 @@ nsresult FetchDriver::HttpFetch(
|
||||
mObserver->OnNotifyNetworkMonitorAlternateStack(httpChan->ChannelId());
|
||||
}
|
||||
|
||||
// Should set a Content-Range header for blob scheme, and also slice the
|
||||
// blob appropriately, so we process the Range header here for later use.
|
||||
if (IsBlobURI(uri)) {
|
||||
ErrorResult result;
|
||||
nsAutoCString range;
|
||||
mRequest->Headers()->Get("Range"_ns, range, result);
|
||||
MOZ_ASSERT(!result.Failed());
|
||||
if (!range.IsVoid()) {
|
||||
rv = NS_SetChannelContentRangeForBlobURI(chan, uri, range);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the preferred alternative data type in InternalRequest is not empty, set
|
||||
// the data type on the created channel and also create a
|
||||
// AlternativeDataStreamListener to be the stream listener of the channel.
|
||||
@ -1057,8 +1073,30 @@ FetchDriver::OnStartRequest(nsIRequest* aRequest) {
|
||||
}
|
||||
MOZ_ASSERT(!result.Failed());
|
||||
} else {
|
||||
response = MakeSafeRefPtr<InternalResponse>(200, "OK"_ns,
|
||||
mRequest->GetCredentialsMode());
|
||||
// Should set a Content-Range header for blob scheme
|
||||
// (https://fetch.spec.whatwg.org/#scheme-fetch)
|
||||
nsAutoCString contentRange(VoidCString());
|
||||
nsCOMPtr<nsIURI> uri;
|
||||
channel->GetURI(getter_AddRefs(uri));
|
||||
if (IsBlobURI(uri)) {
|
||||
nsBaseChannel* bchan = static_cast<nsBaseChannel*>(channel.get());
|
||||
MOZ_ASSERT(bchan);
|
||||
Maybe<nsBaseChannel::ContentRange> range = bchan->GetContentRange();
|
||||
if (range.isSome()) {
|
||||
range->AsHeader(contentRange);
|
||||
}
|
||||
}
|
||||
|
||||
response = MakeSafeRefPtr<InternalResponse>(
|
||||
contentRange.IsVoid() ? 200 : 206,
|
||||
contentRange.IsVoid() ? "OK"_ns : "Partial Content"_ns,
|
||||
mRequest->GetCredentialsMode());
|
||||
|
||||
IgnoredErrorResult result;
|
||||
if (!contentRange.IsVoid()) {
|
||||
response->Headers()->Append("Content-Range"_ns, contentRange, result);
|
||||
MOZ_ASSERT(!result.Failed());
|
||||
}
|
||||
|
||||
if (!contentType.IsEmpty()) {
|
||||
nsAutoCString contentCharset;
|
||||
@ -1068,7 +1106,6 @@ FetchDriver::OnStartRequest(nsIRequest* aRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
IgnoredErrorResult result;
|
||||
response->Headers()->Append("Content-Type"_ns, contentType, result);
|
||||
MOZ_ASSERT(!result.Failed());
|
||||
|
||||
|
@ -481,11 +481,26 @@ void BlobURLInputStream::RetrieveBlobData(const MutexAutoLock& aProofOfLock) {
|
||||
nsresult BlobURLInputStream::StoreBlobImplStream(
|
||||
already_AddRefed<BlobImpl> aBlobImpl, const MutexAutoLock& aProofOfLock) {
|
||||
MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
|
||||
const RefPtr<BlobImpl> blobImpl = aBlobImpl;
|
||||
RefPtr<BlobImpl> blobImpl = aBlobImpl;
|
||||
nsAutoString blobContentType;
|
||||
nsAutoCString channelContentType;
|
||||
|
||||
// If a Range header was in the request then fetch/XHR will have set a
|
||||
// ContentRange on the channel earlier so we may slice the blob now.
|
||||
blobImpl->GetType(blobContentType);
|
||||
const Maybe<nsBaseChannel::ContentRange>& contentRange =
|
||||
mChannel->GetContentRange();
|
||||
if (contentRange.isSome()) {
|
||||
IgnoredErrorResult result;
|
||||
uint64_t start = contentRange->Start();
|
||||
uint64_t end = contentRange->End();
|
||||
RefPtr<BlobImpl> slice =
|
||||
blobImpl->CreateSlice(start, end - start + 1, blobContentType, result);
|
||||
if (!result.Failed()) {
|
||||
blobImpl = slice;
|
||||
}
|
||||
}
|
||||
|
||||
mChannel->GetContentType(channelContentType);
|
||||
// A empty content type is the correct channel content type in the case of a
|
||||
// fetch of a blob where the type was not set. It is invalid in others cases
|
||||
|
@ -945,6 +945,30 @@ nsresult NS_GetBlobForBlobURISpec(const nsACString& aSpec,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Blob requests may specify a range header. We parse, validate, and
|
||||
// store that info here, and save it on the nsBaseChannel, where it
|
||||
// can be accessed by BlobURLInputStream::StoreBlobImplStream.
|
||||
nsresult NS_SetChannelContentRangeForBlobURI(nsIChannel* aChannel, nsIURI* aURI,
|
||||
nsACString& aRangeHeader) {
|
||||
MOZ_ASSERT(aChannel);
|
||||
MOZ_ASSERT(aURI);
|
||||
RefPtr<mozilla::dom::BlobImpl> blobImpl;
|
||||
if (NS_FAILED(NS_GetBlobForBlobURI(aURI, getter_AddRefs(blobImpl)))) {
|
||||
return NS_BINDING_FAILED;
|
||||
}
|
||||
mozilla::IgnoredErrorResult result;
|
||||
int64_t size = static_cast<int64_t>(blobImpl->GetSize(result));
|
||||
if (result.Failed()) {
|
||||
return NS_ERROR_NO_CONTENT;
|
||||
}
|
||||
nsBaseChannel* bchan = static_cast<nsBaseChannel*>(aChannel);
|
||||
MOZ_ASSERT(bchan);
|
||||
if (!bchan->SetContentRange(aRangeHeader, size)) {
|
||||
return NS_ERROR_NET_PARTIAL_TRANSFER;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult NS_GetSourceForMediaSourceURI(nsIURI* aURI,
|
||||
mozilla::dom::MediaSource** aSource) {
|
||||
*aSource = nullptr;
|
||||
|
@ -134,6 +134,10 @@ extern nsresult NS_GetBlobForBlobURISpec(const nsACString& aSpec,
|
||||
mozilla::dom::BlobImpl** aBlob,
|
||||
bool aAlsoIfRevoked = false);
|
||||
|
||||
extern nsresult NS_SetChannelContentRangeForBlobURI(nsIChannel* aChannel,
|
||||
nsIURI* aURI,
|
||||
nsACString& aRangeHeader);
|
||||
|
||||
extern nsresult NS_GetSourceForMediaSourceURI(
|
||||
nsIURI* aURI, mozilla::dom::MediaSource** aSource);
|
||||
|
||||
|
@ -811,6 +811,28 @@ bool XMLHttpRequestMainThread::IsDeniedCrossSiteCORSRequest() {
|
||||
return false;
|
||||
}
|
||||
|
||||
Maybe<nsBaseChannel::ContentRange>
|
||||
XMLHttpRequestMainThread::GetRequestedContentRange() const {
|
||||
MOZ_ASSERT(mChannel);
|
||||
if (!IsBlobURI(mRequestURL)) {
|
||||
return mozilla::Nothing();
|
||||
}
|
||||
nsBaseChannel* baseChan = static_cast<nsBaseChannel*>(mChannel.get());
|
||||
if (!baseChan) {
|
||||
return mozilla::Nothing();
|
||||
}
|
||||
return baseChan->GetContentRange();
|
||||
}
|
||||
|
||||
void XMLHttpRequestMainThread::GetContentRangeHeader(nsACString& out) const {
|
||||
Maybe<nsBaseChannel::ContentRange> range = GetRequestedContentRange();
|
||||
if (range.isSome()) {
|
||||
range->AsHeader(out);
|
||||
} else {
|
||||
out.SetIsVoid(true);
|
||||
}
|
||||
}
|
||||
|
||||
void XMLHttpRequestMainThread::GetResponseURL(nsAString& aUrl) {
|
||||
aUrl.Truncate();
|
||||
|
||||
@ -867,8 +889,8 @@ uint32_t XMLHttpRequestMainThread::GetStatus(ErrorResult& aRv) {
|
||||
|
||||
nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel();
|
||||
if (!httpChannel) {
|
||||
// Pretend like we got a 200 response, since our load was successful
|
||||
return 200;
|
||||
// Pretend like we got a 200/206 response, since our load was successful
|
||||
return GetRequestedContentRange().isSome() ? 206 : 200;
|
||||
}
|
||||
|
||||
uint32_t status;
|
||||
@ -1175,6 +1197,17 @@ void XMLHttpRequestMainThread::GetAllResponseHeaders(
|
||||
aResponseHeaders.AppendLiteral("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Should set a Content-Range header for blob scheme.
|
||||
// From https://fetch.spec.whatwg.org/#scheme-fetch 3.blob.9.20:
|
||||
// "Set response’s header list to «(`Content-Length`, serializedSlicedLength),
|
||||
// (`Content-Type`, type), (`Content-Range`, contentRange)»."
|
||||
GetContentRangeHeader(value);
|
||||
if (!value.IsVoid()) {
|
||||
aResponseHeaders.AppendLiteral("Content-Range: ");
|
||||
aResponseHeaders.Append(value);
|
||||
aResponseHeaders.AppendLiteral("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
void XMLHttpRequestMainThread::GetResponseHeader(const nsACString& header,
|
||||
@ -1229,6 +1262,11 @@ void XMLHttpRequestMainThread::GetResponseHeader(const nsACString& header,
|
||||
}
|
||||
}
|
||||
|
||||
// Content Range:
|
||||
else if (header.LowerCaseEqualsASCII("content-range")) {
|
||||
GetContentRangeHeader(_retval);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1865,6 +1903,13 @@ XMLHttpRequestMainThread::OnStartRequest(nsIRequest* request) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// If we were asked for a bad range on a blob URL, but we're async,
|
||||
// we should throw now in order to fire an error progress event.
|
||||
if (GetRequestedContentRange().isNothing() &&
|
||||
mAuthorRequestHeaders.Has("range")) {
|
||||
return NS_ERROR_NET_PARTIAL_TRANSFER;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIChannel> channel(do_QueryInterface(request));
|
||||
NS_ENSURE_TRUE(channel, NS_ERROR_UNEXPECTED);
|
||||
|
||||
@ -1905,11 +1950,13 @@ XMLHttpRequestMainThread::OnStartRequest(nsIRequest* request) {
|
||||
channel->SetContentType(NS_ConvertUTF16toUTF8(mOverrideMimeType));
|
||||
}
|
||||
|
||||
// Fallback to 'application/octet-stream'
|
||||
nsAutoCString type;
|
||||
channel->GetContentType(type);
|
||||
if (type.IsEmpty() || type.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) {
|
||||
channel->SetContentType(nsLiteralCString(APPLICATION_OCTET_STREAM));
|
||||
// Fallback to 'application/octet-stream' (leaving data URLs alone)
|
||||
if (!IsBlobURI(mRequestURL)) {
|
||||
nsAutoCString type;
|
||||
channel->GetContentType(type);
|
||||
if (type.IsEmpty() || type.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) {
|
||||
channel->SetContentType(nsLiteralCString(APPLICATION_OCTET_STREAM));
|
||||
}
|
||||
}
|
||||
|
||||
DetectCharset();
|
||||
@ -2665,6 +2712,21 @@ nsresult XMLHttpRequestMainThread::InitiateFetch(
|
||||
}
|
||||
}
|
||||
|
||||
// Should set a Content-Range header for blob scheme, and also slice the
|
||||
// blob appropriately, so we process the Range header here for later use.
|
||||
if (IsBlobURI(mRequestURL)) {
|
||||
nsAutoCString range;
|
||||
mAuthorRequestHeaders.Get("range", range);
|
||||
if (!range.IsVoid()) {
|
||||
rv = NS_SetChannelContentRangeForBlobURI(mChannel, mRequestURL, range);
|
||||
if (mFlagSynchronous && NS_FAILED(rv)) {
|
||||
// We later fire an error progress event for non-sync
|
||||
mState = XMLHttpRequest_Binding::DONE;
|
||||
return NS_ERROR_DOM_NETWORK_ERR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Due to the chrome-only XHR.channel API, we need a hacky way to set the
|
||||
// SEC_COOKIES_INCLUDE *after* the channel has been has been created, since
|
||||
// .withCredentials can be called after open() is called.
|
||||
@ -2813,6 +2875,11 @@ already_AddRefed<PreloaderBase> XMLHttpRequestMainThread::FindPreload() {
|
||||
void XMLHttpRequestMainThread::EnsureChannelContentType() {
|
||||
MOZ_ASSERT(mChannel);
|
||||
|
||||
// We don't mess with the content type of a blob URL.
|
||||
if (IsBlobURI(mRequestURL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we expect XML data, set the type hint accordingly
|
||||
// if the channel doesn't know any content type.
|
||||
// This means that we always try to parse local files as XML
|
||||
|
@ -47,6 +47,7 @@
|
||||
#include "mozilla/dom/XMLHttpRequestEventTarget.h"
|
||||
#include "mozilla/dom/XMLHttpRequestString.h"
|
||||
#include "mozilla/Encoding.h"
|
||||
#include "nsBaseChannel.h"
|
||||
|
||||
#ifdef Status
|
||||
/* Xlib headers insist on this for some reason... Nuke it because
|
||||
@ -508,6 +509,9 @@ class XMLHttpRequestMainThread final : public XMLHttpRequest,
|
||||
|
||||
void AbortInternal(ErrorResult& aRv);
|
||||
|
||||
Maybe<nsBaseChannel::ContentRange> GetRequestedContentRange() const;
|
||||
void GetContentRangeHeader(nsACString&) const;
|
||||
|
||||
struct PendingEvent {
|
||||
RefPtr<DOMEventTargetHelper> mTarget;
|
||||
RefPtr<Event> mEvent;
|
||||
|
@ -949,3 +949,59 @@ void nsBaseChannel::SetupNeckoTarget() {
|
||||
mNeckoTarget =
|
||||
nsContentUtils::GetEventTargetByLoadInfo(mLoadInfo, TaskCategory::Other);
|
||||
}
|
||||
|
||||
nsBaseChannel::ContentRange::ContentRange(const nsACString& aRangeHeader,
|
||||
uint64_t aSize)
|
||||
: mStart(0), mEnd(0), mSize(0) {
|
||||
auto parsed = nsContentUtils::ParseSingleRangeRequest(aRangeHeader, true);
|
||||
// https://fetch.spec.whatwg.org/#ref-for-simple-range-header-value%E2%91%A1
|
||||
// If rangeValue is failure, then return a network error.
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanity check: ParseSingleRangeRequest should handle these two cases.
|
||||
// If rangeEndValue and rangeStartValue are null, then return failure.
|
||||
MOZ_ASSERT(parsed->Start().isSome() || parsed->End().isSome());
|
||||
// If rangeStartValue and rangeEndValue are numbers, and rangeStartValue
|
||||
// is greater than rangeEndValue, then return failure.
|
||||
MOZ_ASSERT(parsed->Start().isNothing() || parsed->End().isNothing() ||
|
||||
*parsed->Start() <= *parsed->End());
|
||||
|
||||
// https://fetch.spec.whatwg.org/#ref-for-simple-range-header-value%E2%91%A1
|
||||
// If rangeStart is null:
|
||||
if (parsed->Start().isNothing()) {
|
||||
// Set rangeStart to fullLength − rangeEnd.
|
||||
mStart = aSize - *parsed->End();
|
||||
|
||||
// Set rangeEnd to rangeStart + rangeEnd − 1.
|
||||
mEnd = mStart + *parsed->End() - 1;
|
||||
|
||||
// Otherwise:
|
||||
} else {
|
||||
// If rangeStart is greater than or equal to fullLength, then return a
|
||||
// network error.
|
||||
if (*parsed->Start() >= aSize) {
|
||||
return;
|
||||
}
|
||||
mStart = *parsed->Start();
|
||||
|
||||
// If rangeEnd is null or rangeEnd is greater than or equal to fullLength,
|
||||
// then set rangeEnd to fullLength − 1.
|
||||
if (parsed->End().isNothing() || *parsed->End() >= aSize) {
|
||||
mEnd = aSize - 1;
|
||||
} else {
|
||||
mEnd = *parsed->End();
|
||||
}
|
||||
}
|
||||
mSize = aSize;
|
||||
}
|
||||
|
||||
void nsBaseChannel::ContentRange::AsHeader(nsACString& aOutString) const {
|
||||
aOutString.Assign("bytes "_ns);
|
||||
aOutString.AppendInt(mStart);
|
||||
aOutString.AppendLiteral("-");
|
||||
aOutString.AppendInt(mEnd);
|
||||
aOutString.AppendLiteral("/");
|
||||
aOutString.AppendInt(mSize);
|
||||
}
|
||||
|
@ -198,6 +198,44 @@ class nsBaseChannel
|
||||
return mPumpingData || mWaitingOnAsyncRedirect;
|
||||
}
|
||||
|
||||
// Blob requests may specify a range header. We must parse, validate, and
|
||||
// store that info in a place where BlobURLInputStream::StoreBlobImplStream
|
||||
// can access it. This class helps to encapsulate that logic.
|
||||
class ContentRange {
|
||||
private:
|
||||
uint64_t mStart;
|
||||
uint64_t mEnd;
|
||||
uint64_t mSize;
|
||||
|
||||
public:
|
||||
uint64_t Start() const { return mStart; }
|
||||
uint64_t End() const { return mEnd; }
|
||||
uint64_t Size() const { return mSize; }
|
||||
bool IsValid() const { return mStart < mSize; }
|
||||
ContentRange() : mStart(0), mEnd(0), mSize(0) {}
|
||||
ContentRange(uint64_t aStart, uint64_t aEnd, uint64_t aSize)
|
||||
: mStart(aStart), mEnd(aEnd), mSize(aSize) {}
|
||||
ContentRange(const nsACString& aRangeHeader, uint64_t aSize);
|
||||
void AsHeader(nsACString& aOutString) const;
|
||||
};
|
||||
|
||||
const mozilla::Maybe<ContentRange>& GetContentRange() const {
|
||||
return mContentRange;
|
||||
}
|
||||
|
||||
void SetContentRange(uint64_t aStart, uint64_t aEnd, uint64_t aSize) {
|
||||
mContentRange.emplace(ContentRange(aStart, aEnd, aSize));
|
||||
}
|
||||
|
||||
bool SetContentRange(const nsACString& aRangeHeader, uint64_t aSize) {
|
||||
auto range = ContentRange(aRangeHeader, aSize);
|
||||
if (!range.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
mContentRange.emplace(range);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper function for querying the channel's notification callbacks.
|
||||
template <class T>
|
||||
void GetCallback(nsCOMPtr<T>& result) {
|
||||
@ -288,6 +326,7 @@ class nsBaseChannel
|
||||
bool mWaitingOnAsyncRedirect{false};
|
||||
bool mOpenRedirectChannel{false};
|
||||
uint32_t mRedirectFlags{0};
|
||||
mozilla::Maybe<ContentRange> mContentRange;
|
||||
|
||||
protected:
|
||||
nsCString mContentType;
|
||||
|
@ -1,158 +0,0 @@
|
||||
[blob.any.worker.html]
|
||||
[A simple blob range request.]
|
||||
expected: FAIL
|
||||
|
||||
[A blob range request with no end.]
|
||||
expected: FAIL
|
||||
|
||||
[A blob range request with no start.]
|
||||
expected: FAIL
|
||||
|
||||
[A simple blob range request with whitespace.]
|
||||
expected: FAIL
|
||||
|
||||
[Blob content with short content and a large range end]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range request with multiple range values]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range request with multiple range values and whitespace]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range request with trailing comma]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with no start or end]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range request with short range end]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range start should be an ASCII digit]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range should have a dash]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range end should be an ASCII digit]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range should include '-']
|
||||
expected: FAIL
|
||||
|
||||
[Blob range should include '=']
|
||||
expected: FAIL
|
||||
|
||||
[Blob range should include 'bytes=']
|
||||
expected: FAIL
|
||||
|
||||
[Blob content with short content and a large range start]
|
||||
expected: FAIL
|
||||
|
||||
[Blob content with short content and a range end matching content length]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with whitespace before and after hyphen]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with whitespace after hyphen]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with whitespace around equals sign]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with no value]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with incorrect range header]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with incorrect range header #2]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with incorrect range header #3]
|
||||
expected: FAIL
|
||||
|
||||
[Blob content with short content and a range start matching the content length]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[blob.any.html]
|
||||
[A simple blob range request.]
|
||||
expected: FAIL
|
||||
|
||||
[A blob range request with no end.]
|
||||
expected: FAIL
|
||||
|
||||
[A blob range request with no start.]
|
||||
expected: FAIL
|
||||
|
||||
[A simple blob range request with whitespace.]
|
||||
expected: FAIL
|
||||
|
||||
[Blob content with short content and a large range end]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range request with multiple range values]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range request with multiple range values and whitespace]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range request with trailing comma]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with no start or end]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range request with short range end]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range start should be an ASCII digit]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range should have a dash]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range end should be an ASCII digit]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range should include '-']
|
||||
expected: FAIL
|
||||
|
||||
[Blob range should include '=']
|
||||
expected: FAIL
|
||||
|
||||
[Blob range should include 'bytes=']
|
||||
expected: FAIL
|
||||
|
||||
[Blob content with short content and a large range start]
|
||||
expected: FAIL
|
||||
|
||||
[Blob content with short content and a range end matching content length]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with whitespace before and after hyphen]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with whitespace after hyphen]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with whitespace around equals sign]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with no value]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with incorrect range header]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with incorrect range header #2]
|
||||
expected: FAIL
|
||||
|
||||
[Blob range with incorrect range header #3]
|
||||
expected: FAIL
|
||||
|
||||
[Blob content with short content and a range start matching the content length]
|
||||
expected: FAIL
|
@ -10,6 +10,15 @@ const supportedBlobRange = [
|
||||
content_range: "bytes 9-21/30",
|
||||
result: "Hello, World!",
|
||||
},
|
||||
{
|
||||
name: "A blob range request with no type.",
|
||||
data: ["A simple Hello, World! example"],
|
||||
type: undefined,
|
||||
range: "bytes=9-21",
|
||||
content_length: 13,
|
||||
content_range: "bytes 9-21/30",
|
||||
result: "Hello, World!",
|
||||
},
|
||||
{
|
||||
name: "A blob range request with no end.",
|
||||
data: ["Range with no end"],
|
||||
@ -201,7 +210,7 @@ supportedBlobRange.forEach(({ name, data, type, range, content_length, content_r
|
||||
});
|
||||
assert_equals(resp.status, 206, "HTTP status is 206");
|
||||
assert_equals(resp.type, "basic", "response type is basic");
|
||||
assert_equals(resp.headers.get("Content-Type"), type, "Content-Type is " + resp.headers.get("Content-Type"));
|
||||
assert_equals(resp.headers.get("Content-Type"), type || "", "Content-Type is " + resp.headers.get("Content-Type"));
|
||||
assert_equals(resp.headers.get("Content-Length"), content_length.toString(), "Content-Length is " + resp.headers.get("Content-Length"));
|
||||
assert_equals(resp.headers.get("Content-Range"), content_range, "Content-Range is " + resp.headers.get("Content-Range"));
|
||||
const text = await resp.text();
|
||||
|
246
testing/web-platform/tests/xhr/blob-range.any.js
Normal file
246
testing/web-platform/tests/xhr/blob-range.any.js
Normal file
@ -0,0 +1,246 @@
|
||||
// See also /fetch/range/blob.any.js
|
||||
|
||||
const supportedBlobRange = [
|
||||
{
|
||||
name: "A simple blob range request.",
|
||||
data: ["A simple Hello, World! example"],
|
||||
type: "text/plain",
|
||||
range: "bytes=9-21",
|
||||
content_length: 13,
|
||||
content_range: "bytes 9-21/30",
|
||||
result: "Hello, World!",
|
||||
},
|
||||
{
|
||||
name: "A blob range request with no type.",
|
||||
data: ["A simple Hello, World! example"],
|
||||
type: undefined,
|
||||
range: "bytes=9-21",
|
||||
content_length: 13,
|
||||
content_range: "bytes 9-21/30",
|
||||
result: "Hello, World!",
|
||||
},
|
||||
{
|
||||
name: "A blob range request with no end.",
|
||||
data: ["Range with no end"],
|
||||
type: "text/plain",
|
||||
range: "bytes=11-",
|
||||
content_length: 6,
|
||||
content_range: "bytes 11-16/17",
|
||||
result: "no end",
|
||||
},
|
||||
{
|
||||
name: "A blob range request with no start.",
|
||||
data: ["Range with no start"],
|
||||
type: "text/plain",
|
||||
range: "bytes=-8",
|
||||
content_length: 8,
|
||||
content_range: "bytes 11-18/19",
|
||||
result: "no start",
|
||||
},
|
||||
{
|
||||
name: "A simple blob range request with whitespace.",
|
||||
data: ["A simple Hello, World! example"],
|
||||
type: "text/plain",
|
||||
range: "bytes= \t9-21",
|
||||
content_length: 13,
|
||||
content_range: "bytes 9-21/30",
|
||||
result: "Hello, World!",
|
||||
},
|
||||
{
|
||||
name: "Blob content with short content and a large range end",
|
||||
data: ["Not much here"],
|
||||
type: "text/plain",
|
||||
range: "bytes=4-100000000000",
|
||||
content_length: 9,
|
||||
content_range: "bytes 4-12/13",
|
||||
result: "much here",
|
||||
},
|
||||
{
|
||||
name: "Blob content with short content and a range end matching content length",
|
||||
data: ["Not much here"],
|
||||
type: "text/plain",
|
||||
range: "bytes=4-13",
|
||||
content_length: 9,
|
||||
content_range: "bytes 4-12/13",
|
||||
result: "much here",
|
||||
},
|
||||
{
|
||||
name: "Blob range with whitespace before and after hyphen",
|
||||
data: ["Valid whitespace #1"],
|
||||
type: "text/plain",
|
||||
range: "bytes=5 - 10",
|
||||
content_length: 6,
|
||||
content_range: "bytes 5-10/19",
|
||||
result: " white",
|
||||
},
|
||||
{
|
||||
name: "Blob range with whitespace after hyphen",
|
||||
data: ["Valid whitespace #2"],
|
||||
type: "text/plain",
|
||||
range: "bytes=-\t 5",
|
||||
content_length: 5,
|
||||
content_range: "bytes 14-18/19",
|
||||
result: "ce #2",
|
||||
},
|
||||
{
|
||||
name: "Blob range with whitespace around equals sign",
|
||||
data: ["Valid whitespace #3"],
|
||||
type: "text/plain",
|
||||
range: "bytes \t =\t 6-",
|
||||
content_length: 13,
|
||||
content_range: "bytes 6-18/19",
|
||||
result: "whitespace #3",
|
||||
},
|
||||
];
|
||||
|
||||
const unsupportedBlobRange = [
|
||||
{
|
||||
name: "Blob range with no value",
|
||||
data: ["Blob range should have a value"],
|
||||
type: "text/plain",
|
||||
range: "",
|
||||
},
|
||||
{
|
||||
name: "Blob range with incorrect range header",
|
||||
data: ["A"],
|
||||
type: "text/plain",
|
||||
range: "byte=0-"
|
||||
},
|
||||
{
|
||||
name: "Blob range with incorrect range header #2",
|
||||
data: ["A"],
|
||||
type: "text/plain",
|
||||
range: "bytes"
|
||||
},
|
||||
{
|
||||
name: "Blob range with incorrect range header #3",
|
||||
data: ["A"],
|
||||
type: "text/plain",
|
||||
range: "bytes\t \t"
|
||||
},
|
||||
{
|
||||
name: "Blob range request with multiple range values",
|
||||
data: ["Multiple ranges are not currently supported"],
|
||||
type: "text/plain",
|
||||
range: "bytes=0-5,15-",
|
||||
},
|
||||
{
|
||||
name: "Blob range request with multiple range values and whitespace",
|
||||
data: ["Multiple ranges are not currently supported"],
|
||||
type: "text/plain",
|
||||
range: "bytes=0-5, 15-",
|
||||
},
|
||||
{
|
||||
name: "Blob range request with trailing comma",
|
||||
data: ["Range with invalid trailing comma"],
|
||||
type: "text/plain",
|
||||
range: "bytes=0-5,",
|
||||
},
|
||||
{
|
||||
name: "Blob range with no start or end",
|
||||
data: ["Range with no start or end"],
|
||||
type: "text/plain",
|
||||
range: "bytes=-",
|
||||
},
|
||||
{
|
||||
name: "Blob range request with short range end",
|
||||
data: ["Range end should be greater than range start"],
|
||||
type: "text/plain",
|
||||
range: "bytes=10-5",
|
||||
},
|
||||
{
|
||||
name: "Blob range start should be an ASCII digit",
|
||||
data: ["Range start must be an ASCII digit"],
|
||||
type: "text/plain",
|
||||
range: "bytes=x-5",
|
||||
},
|
||||
{
|
||||
name: "Blob range should have a dash",
|
||||
data: ["Blob range should have a dash"],
|
||||
type: "text/plain",
|
||||
range: "bytes=5",
|
||||
},
|
||||
{
|
||||
name: "Blob range end should be an ASCII digit",
|
||||
data: ["Range end must be an ASCII digit"],
|
||||
type: "text/plain",
|
||||
range: "bytes=5-x",
|
||||
},
|
||||
{
|
||||
name: "Blob range should include '-'",
|
||||
data: ["Range end must include '-'"],
|
||||
type: "text/plain",
|
||||
range: "bytes=x",
|
||||
},
|
||||
{
|
||||
name: "Blob range should include '='",
|
||||
data: ["Range end must include '='"],
|
||||
type: "text/plain",
|
||||
range: "bytes 5-",
|
||||
},
|
||||
{
|
||||
name: "Blob range should include 'bytes='",
|
||||
data: ["Range end must include 'bytes='"],
|
||||
type: "text/plain",
|
||||
range: "5-",
|
||||
},
|
||||
{
|
||||
name: "Blob content with short content and a large range start",
|
||||
data: ["Not much here"],
|
||||
type: "text/plain",
|
||||
range: "bytes=100000-",
|
||||
},
|
||||
{
|
||||
name: "Blob content with short content and a range start matching the content length",
|
||||
data: ["Not much here"],
|
||||
type: "text/plain",
|
||||
range: "bytes=13-",
|
||||
},
|
||||
];
|
||||
|
||||
supportedBlobRange.forEach(({ name, data, type, range, content_length, content_range, result }) => {
|
||||
promise_test(async t => {
|
||||
const blob = new Blob(data, { "type" : type });
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
t.add_cleanup(() => URL.revokeObjectURL(blobURL));
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", blobURL);
|
||||
xhr.responseType = "text";
|
||||
xhr.setRequestHeader("Range", range);
|
||||
await new Promise(resolve => {
|
||||
xhr.onloadend = resolve;
|
||||
xhr.send();
|
||||
});
|
||||
assert_equals(xhr.status, 206, "HTTP status is 206");
|
||||
assert_equals(xhr.getResponseHeader("Content-Type"), type || "", "Content-Type is " + xhr.getResponseHeader("Content-Type"));
|
||||
assert_equals(xhr.getResponseHeader("Content-Length"), content_length.toString(), "Content-Length is " + xhr.getResponseHeader("Content-Length"));
|
||||
assert_equals(xhr.getResponseHeader("Content-Range"), content_range, "Content-Range is " + xhr.getResponseHeader("Content-Range"));
|
||||
assert_equals(xhr.responseText, result, "Response's body is correct");
|
||||
const all = xhr.getAllResponseHeaders().toLowerCase();
|
||||
assert_true(all.includes(`content-type: ${type || ""}`), "Expected Content-Type in getAllResponseHeaders()");
|
||||
assert_true(all.includes(`content-length: ${content_length}`), "Expected Content-Length in getAllResponseHeaders()");
|
||||
assert_true(all.includes(`content-range: ${content_range}`), "Expected Content-Range in getAllResponseHeaders()")
|
||||
}, name);
|
||||
});
|
||||
|
||||
unsupportedBlobRange.forEach(({ name, data, type, range }) => {
|
||||
promise_test(t => {
|
||||
const blob = new Blob(data, { "type" : type });
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
t.add_cleanup(() => URL.revokeObjectURL(blobURL));
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", blobURL, false);
|
||||
xhr.setRequestHeader("Range", range);
|
||||
assert_throws_dom("NetworkError", () => xhr.send());
|
||||
|
||||
xhr.open("GET", blobURL);
|
||||
xhr.setRequestHeader("Range", range);
|
||||
xhr.responseType = "text";
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.onload = reject;
|
||||
xhr.onerror = resolve;
|
||||
xhr.send();
|
||||
});
|
||||
}, name);
|
||||
});
|
Loading…
Reference in New Issue
Block a user