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:
Thomas Wisniewski 2023-08-01 02:48:47 +00:00
parent 5361e039c9
commit 44b6f2153c
13 changed files with 522 additions and 179 deletions

View File

@ -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);
}

View File

@ -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;
};
/**

View File

@ -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());

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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 responses 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

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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

View File

@ -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();

View 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);
});