feat(http): add support for ArrayBuffer
Add the buffer option to store response in ArrayBuffer Improve the interface to get back response independently of the buffer type
This commit is contained in:
parent
0ccb6e0dfc
commit
1266460386
|
@ -28,7 +28,7 @@ export {JSONPBackend, JSONPConnection} from './src/backends/jsonp_backend';
|
|||
export {CookieXSRFStrategy, XHRBackend, XHRConnection} from './src/backends/xhr_backend';
|
||||
export {BaseRequestOptions, RequestOptions} from './src/base_request_options';
|
||||
export {BaseResponseOptions, ResponseOptions} from './src/base_response_options';
|
||||
export {ReadyState, RequestMethod, ResponseType} from './src/enums';
|
||||
export {ReadyState, RequestMethod, ResponseBuffer, ResponseType} from './src/enums';
|
||||
export {Headers} from './src/headers';
|
||||
export {Http, Jsonp} from './src/http';
|
||||
export {Connection, ConnectionBackend, RequestOptionsArgs, ResponseOptionsArgs, XSRFStrategy} from './src/interfaces';
|
||||
|
|
|
@ -12,7 +12,7 @@ import {Observable} from 'rxjs/Observable';
|
|||
import {Observer} from 'rxjs/Observer';
|
||||
|
||||
import {ResponseOptions} from '../base_response_options';
|
||||
import {ContentType, ReadyState, RequestMethod, ResponseType} from '../enums';
|
||||
import {ContentType, ReadyState, RequestMethod, ResponseType, ResponseBuffer} from '../enums';
|
||||
import {isPresent, isString} from '../facade/lang';
|
||||
import {Headers} from '../headers';
|
||||
import {getResponseURL, isSuccess} from '../http_utils';
|
||||
|
@ -24,6 +24,7 @@ import {BrowserXhr} from './browser_xhr';
|
|||
|
||||
const XSSI_PREFIX = /^\)\]\}',?\n/;
|
||||
|
||||
|
||||
/**
|
||||
* Creates connections using `XMLHttpRequest`. Given a fully-qualified
|
||||
* request, an `XHRConnection` will immediately create an `XMLHttpRequest` object and send the
|
||||
|
@ -51,7 +52,7 @@ export class XHRConnection implements Connection {
|
|||
_xhr.withCredentials = req.withCredentials;
|
||||
}
|
||||
// load event handler
|
||||
let onLoad = () => {
|
||||
let onLoad = () => {
|
||||
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
|
||||
// response/responseType properties were introduced in XHR Level2 spec (supported by
|
||||
// IE10)
|
||||
|
@ -108,6 +109,20 @@ export class XHRConnection implements Connection {
|
|||
req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(',')));
|
||||
}
|
||||
|
||||
// Select the correct buffer type to store the response
|
||||
if (isPresent(req.buffer) && isPresent(_xhr.responseType)) switch (req.buffer) {
|
||||
case ResponseBuffer.ArrayBuffer:
|
||||
_xhr.responseType = "arraybuffer";
|
||||
break;
|
||||
case ResponseBuffer.Json:
|
||||
_xhr.responseType = "json";
|
||||
break;
|
||||
default:
|
||||
case ResponseBuffer.Text:
|
||||
_xhr.responseType = "text";
|
||||
break;
|
||||
}
|
||||
|
||||
_xhr.addEventListener('load', onLoad);
|
||||
_xhr.addEventListener('error', onError);
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {Injectable} from '@angular/core';
|
|||
|
||||
import {isPresent, isString} from '../src/facade/lang';
|
||||
|
||||
import {RequestMethod} from './enums';
|
||||
import {RequestMethod, ResponseBuffer} from './enums';
|
||||
import {Headers} from './headers';
|
||||
import {normalizeMethodName} from './http_utils';
|
||||
import {RequestOptionsArgs} from './interfaces';
|
||||
|
@ -69,7 +69,13 @@ export class RequestOptions {
|
|||
* Enable use credentials for a {@link Request}.
|
||||
*/
|
||||
withCredentials: boolean;
|
||||
constructor({method, headers, body, url, search, withCredentials}: RequestOptionsArgs = {}) {
|
||||
/*
|
||||
* Select a buffer to store the response, such as ArrayBuffer, Blob, Json (or Document)
|
||||
*/
|
||||
buffer: ResponseBuffer;
|
||||
|
||||
constructor({method, headers, body, url, search, withCredentials, buffer}: RequestOptionsArgs = {}) {
|
||||
|
||||
this.method = isPresent(method) ? normalizeMethodName(method) : null;
|
||||
this.headers = isPresent(headers) ? headers : null;
|
||||
this.body = isPresent(body) ? body : null;
|
||||
|
@ -78,6 +84,7 @@ export class RequestOptions {
|
|||
(isString(search) ? new URLSearchParams(<string>(search)) : <URLSearchParams>(search)) :
|
||||
null;
|
||||
this.withCredentials = isPresent(withCredentials) ? withCredentials : null;
|
||||
this.buffer = isPresent(buffer) ? buffer : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,7 +124,8 @@ export class RequestOptions {
|
|||
this.search,
|
||||
withCredentials: isPresent(options) && isPresent(options.withCredentials) ?
|
||||
options.withCredentials :
|
||||
this.withCredentials
|
||||
this.withCredentials,
|
||||
buffer: isPresent(options) && isPresent(options.buffer) ? options.buffer : this.buffer
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,11 +44,11 @@ import {ResponseOptionsArgs} from './interfaces';
|
|||
* @experimental
|
||||
*/
|
||||
export class ResponseOptions {
|
||||
// TODO: ArrayBuffer | FormData | Blob
|
||||
// TODO: FormData | Blob
|
||||
/**
|
||||
* String or Object representing the body of the {@link Response}.
|
||||
* String, Object, ArrayBuffer representing the body of the {@link Response}.
|
||||
*/
|
||||
body: string|Object;
|
||||
body: string | Object | ArrayBuffer;
|
||||
/**
|
||||
* Http {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html status code}
|
||||
* associated with the response.
|
||||
|
|
|
@ -61,3 +61,12 @@ export enum ContentType {
|
|||
BLOB,
|
||||
ARRAY_BUFFER
|
||||
}
|
||||
|
||||
/*
|
||||
* Define which buffer to use to store the response
|
||||
*/
|
||||
export enum ResponseBuffer {
|
||||
ArrayBuffer,
|
||||
Json,
|
||||
Text
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ function mergeOptions(
|
|||
search: providedOpts.search,
|
||||
headers: providedOpts.headers,
|
||||
body: providedOpts.body,
|
||||
withCredentials: providedOpts.withCredentials
|
||||
withCredentials: providedOpts.withCredentials,
|
||||
buffer: providedOpts.buffer
|
||||
}));
|
||||
}
|
||||
if (isPresent(method)) {
|
||||
|
|
|
@ -38,4 +38,12 @@ export function getResponseURL(xhr: any): string {
|
|||
return;
|
||||
}
|
||||
|
||||
export function stringToArrayBuffer(string: String): ArrayBuffer {
|
||||
let view = new Uint16Array(string.length);
|
||||
for (var i = 0, strLen = string.length; i < strLen; i++) {
|
||||
view[i] = string.charCodeAt(i);
|
||||
}
|
||||
return view.buffer;
|
||||
}
|
||||
|
||||
export {isJsObject} from '../src/facade/lang';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ReadyState, RequestMethod, ResponseType} from './enums';
|
||||
import {ReadyState, RequestMethod, ResponseType, ResponseBuffer} from './enums';
|
||||
import {Headers} from './headers';
|
||||
import {Request} from './static_request';
|
||||
import {URLSearchParams} from './url_search_params';
|
||||
|
@ -52,6 +52,7 @@ export interface RequestOptionsArgs {
|
|||
headers?: Headers;
|
||||
body?: any;
|
||||
withCredentials?: boolean;
|
||||
buffer?: ResponseBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,8 +67,8 @@ export interface RequestArgs extends RequestOptionsArgs { url: string; }
|
|||
* @experimental
|
||||
*/
|
||||
export type ResponseOptionsArgs = {
|
||||
// TODO: Support Blob, ArrayBuffer, JSON
|
||||
body?: string | Object | FormData; status?: number; statusText?: string; headers?: Headers;
|
||||
// TODO: Support Blob, JSON
|
||||
body?: string | Object | FormData | ArrayBuffer; status?: number; statusText?: string; headers?: Headers;
|
||||
type?: ResponseType;
|
||||
url?: string;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {StringWrapper, isPresent} from '../src/facade/lang';
|
||||
|
||||
import {ContentType, RequestMethod} from './enums';
|
||||
import {ContentType, RequestMethod, ResponseBuffer} from './enums';
|
||||
import {Headers} from './headers';
|
||||
import {normalizeMethodName} from './http_utils';
|
||||
import {RequestArgs} from './interfaces';
|
||||
|
@ -72,6 +72,8 @@ export class Request {
|
|||
private contentType: ContentType;
|
||||
/** Enable use credentials */
|
||||
withCredentials: boolean;
|
||||
/* Select a buffer to store the response */
|
||||
buffer: ResponseBuffer;
|
||||
constructor(requestOptions: RequestArgs) {
|
||||
// TODO: assert that url is present
|
||||
let url = requestOptions.url;
|
||||
|
@ -95,6 +97,7 @@ export class Request {
|
|||
// TODO(jeffbcross): implement behavior
|
||||
this.headers = new Headers(requestOptions.headers);
|
||||
this.withCredentials = requestOptions.withCredentials;
|
||||
this.buffer = requestOptions.buffer;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {Json, isString} from '../src/facade/lang';
|
|||
import {ResponseOptions} from './base_response_options';
|
||||
import {ResponseType} from './enums';
|
||||
import {Headers} from './headers';
|
||||
import {isJsObject} from './http_utils';
|
||||
import {isJsObject, stringToArrayBuffer} from './http_utils';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -83,8 +83,10 @@ export class Response {
|
|||
* Spec](https://fetch.spec.whatwg.org/#headers-class).
|
||||
*/
|
||||
headers: Headers;
|
||||
// TODO: Support ArrayBuffer, JSON, FormData, Blob
|
||||
private _body: string|Object;
|
||||
|
||||
// TODO: Support FormData, Blob
|
||||
private _body: string | Object | ArrayBuffer;
|
||||
|
||||
constructor(responseOptions: ResponseOptions) {
|
||||
this._body = responseOptions.body;
|
||||
this.status = responseOptions.status;
|
||||
|
@ -95,12 +97,6 @@ export class Response {
|
|||
this.url = responseOptions.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented
|
||||
*/
|
||||
// TODO: Blob return type
|
||||
blob(): any { throw new BaseException('"blob()" method not implemented on Response superclass'); }
|
||||
|
||||
/**
|
||||
* Attempts to return body as parsed `JSON` object, or raises an exception.
|
||||
*/
|
||||
|
@ -110,6 +106,10 @@ export class Response {
|
|||
jsonResponse = this._body;
|
||||
} else if (isString(this._body)) {
|
||||
jsonResponse = Json.parse(<string>this._body);
|
||||
} else if (this._body instanceof ArrayBuffer) {
|
||||
jsonResponse = Json.parse(this.text());
|
||||
} else {
|
||||
jsonResponse = this._body;
|
||||
}
|
||||
return jsonResponse;
|
||||
}
|
||||
|
@ -117,17 +117,40 @@ export class Response {
|
|||
/**
|
||||
* Returns the body as a string, presuming `toString()` can be called on the response body.
|
||||
*/
|
||||
text(): string { return this._body.toString(); }
|
||||
text(): string {
|
||||
var textResponse: string;
|
||||
if (this._body instanceof ArrayBuffer) {
|
||||
textResponse = String.fromCharCode.apply(null, new Uint16Array(<ArrayBuffer>this._body));
|
||||
} else if (isJsObject(this._body)) {
|
||||
textResponse = Json.stringify(this._body);
|
||||
} else {
|
||||
textResponse = this._body.toString();
|
||||
}
|
||||
return textResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented
|
||||
* Return the body as an ArrayBuffer
|
||||
*/
|
||||
// TODO: ArrayBuffer return type
|
||||
arrayBuffer(): any {
|
||||
throw new BaseException('"arrayBuffer()" method not implemented on Response superclass');
|
||||
arrayBuffer(): ArrayBuffer {
|
||||
var bufferResponse: ArrayBuffer;
|
||||
if (this._body instanceof ArrayBuffer) {
|
||||
bufferResponse = <ArrayBuffer>this._body;
|
||||
} else {
|
||||
bufferResponse = stringToArrayBuffer(this.text());
|
||||
}
|
||||
return bufferResponse;
|
||||
}
|
||||
|
||||
|
||||
toString(): string {
|
||||
return `Response with status: ${this.status} ${this.statusText} for URL: ${this.url}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented
|
||||
*/
|
||||
// TODO: Blob return type
|
||||
blob(): any { throw new BaseException('"blob()" method not implemented on Response superclass'); }
|
||||
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ class MockBrowserXHR extends BrowserXhr {
|
|||
send: any;
|
||||
open: any;
|
||||
response: any;
|
||||
responseType: string;
|
||||
responseText: string;
|
||||
setRequestHeader: any;
|
||||
callbacks = new Map<string, Function>();
|
||||
|
|
|
@ -10,9 +10,12 @@ import {Injector, ReflectiveInjector, provide} from '@angular/core';
|
|||
import {afterEach, beforeEach, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal';
|
||||
import {AsyncTestCompleter} from '@angular/core/testing/testing_internal';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/zip';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {stringToArrayBuffer} from "../src/http_utils";
|
||||
import {Json} from "../src/facade/lang";
|
||||
|
||||
import {BaseRequestOptions, ConnectionBackend, HTTP_PROVIDERS, Http, JSONPBackend, JSONP_PROVIDERS, Jsonp, Request, RequestMethod, RequestOptions, Response, ResponseOptions, URLSearchParams, XHRBackend} from '../http';
|
||||
import {BaseRequestOptions, ConnectionBackend, HTTP_PROVIDERS, Http, JSONPBackend, JSONP_PROVIDERS, Jsonp, Request, RequestMethod, RequestOptions, Response, ResponseBuffer, ResponseOptions, URLSearchParams, XHRBackend} from '../http';
|
||||
import {MockBackend, MockConnection} from '../testing/mock_backend';
|
||||
|
||||
export function main() {
|
||||
|
@ -379,5 +382,122 @@ export function main() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('response buffer', () => {
|
||||
|
||||
it('should attach the provided buffer to the response',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
backend.connections.subscribe((c: MockConnection) => {
|
||||
expect(c.request.buffer).toBe(ResponseBuffer.ArrayBuffer);
|
||||
c.mockRespond(new Response(new ResponseOptions({body: new ArrayBuffer(32)})));
|
||||
async.done();
|
||||
});
|
||||
http.get('https://www.google.com',
|
||||
new RequestOptions({buffer: ResponseBuffer.ArrayBuffer}))
|
||||
.subscribe((res: Response) => {});
|
||||
}));
|
||||
|
||||
it('should be able to consume a buffer containing a String as any response type',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(baseResponse));
|
||||
http.get('https://www.google.com')
|
||||
.subscribe((res: Response) => {
|
||||
expect(res.arrayBuffer()).toBeAnInstanceOf(ArrayBuffer);
|
||||
expect(res.text()).toBe("base response");
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should be able to consume a buffer containing an ArrayBuffer as any response type',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
let arrayBuffer = stringToArrayBuffer('{"response": "ok"}');
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(new Response(
|
||||
new ResponseOptions({body: arrayBuffer}))));
|
||||
http.get('https://www.google.com')
|
||||
.subscribe((res: Response) => {
|
||||
expect(res.arrayBuffer()).toBe(arrayBuffer);
|
||||
expect(res.text()).toEqual('{"response": "ok"}');
|
||||
expect(res.json()).toEqual({response: "ok"});
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should be able to consume a buffer containing an Object as any response type',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
let simpleObject = {"content": "ok"};
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(new Response(
|
||||
new ResponseOptions({body: simpleObject}))));
|
||||
http.get('https://www.google.com')
|
||||
.subscribe((res: Response) => {
|
||||
expect(res.arrayBuffer()).toBeAnInstanceOf(ArrayBuffer);
|
||||
expect(res.text()).toEqual(Json.stringify(simpleObject));
|
||||
expect(res.json()).toBe(simpleObject);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should preserve encoding of ArrayBuffer response',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
let message = "é@θЂ";
|
||||
let arrayBuffer = stringToArrayBuffer(message);
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(new Response(
|
||||
new ResponseOptions({body: arrayBuffer}))));
|
||||
http.get('https://www.google.com')
|
||||
.subscribe((res: Response) => {
|
||||
expect(res.arrayBuffer()).toBeAnInstanceOf(ArrayBuffer);
|
||||
expect(res.text()).toEqual(message);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should preserve encoding of String response',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
let message = "é@θЂ";
|
||||
backend.connections.subscribe((c: MockConnection) => c.mockRespond(
|
||||
new Response(new ResponseOptions({body: message}))));
|
||||
http.get('https://www.google.com')
|
||||
.subscribe((res: Response) => {
|
||||
expect(res.arrayBuffer()).toEqual(stringToArrayBuffer(message));
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should have an equivalent response independently of the buffer used',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
let message = {"param": "content"};
|
||||
|
||||
backend.connections.subscribe((c: MockConnection) => {
|
||||
let body = (): any => {
|
||||
switch (c.request.buffer) {
|
||||
case ResponseBuffer.Text:
|
||||
return Json.stringify(message);
|
||||
case ResponseBuffer.Json:
|
||||
return message;
|
||||
case ResponseBuffer.ArrayBuffer:
|
||||
return stringToArrayBuffer(Json.stringify(message));
|
||||
}
|
||||
};
|
||||
c.mockRespond(new Response(new ResponseOptions({body: body()})))
|
||||
});
|
||||
|
||||
Observable.zip(http.get('https://www.google.com',
|
||||
new RequestOptions({buffer: ResponseBuffer.Text})),
|
||||
http.get('https://www.google.com',
|
||||
new RequestOptions({buffer: ResponseBuffer.Json})),
|
||||
http.get('https://www.google.com',
|
||||
new RequestOptions({buffer: ResponseBuffer.ArrayBuffer})),
|
||||
(x, y, z) => [x, y, z])
|
||||
.subscribe((res: Array<Response>) => {
|
||||
expect(res[0].text()).toEqual(res[1].text());
|
||||
expect(res[1].text()).toEqual(res[2].text());
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue