feat(http): automatically set request Content-Type header based on body type
Implement the ability to provide objects as request body. The following use cases are supported: * raw objects: a JSON payload is created and the content type set to `application/json` * text: the text is used as it is and no content type header is automatically added * URLSearchParams: a form payload is created and the content type set to `application/x-www-form-urlencoded` * FormData: the object is used as it is and no content type header is automatically added * Blob: the object is used as it is and the content type set with the value of its `type` property if any * ArrayBuffer: the object is used as it is and no content type header is automatically added Closes https://github.com/angular/http/issues/69 Closes #7310
This commit is contained in:
parent
e0c83f669e
commit
0f0a8ade7c
|
@ -7,3 +7,4 @@ import {ReflectionCapabilities} from './reflection_capabilities';
|
||||||
* about symbols.
|
* about symbols.
|
||||||
*/
|
*/
|
||||||
export var reflector = new Reflector(new ReflectionCapabilities());
|
export var reflector = new Reflector(new ReflectionCapabilities());
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {isPresent, isString} from '../../src/facade/lang';
|
||||||
import {Observable} from 'rxjs/Observable';
|
import {Observable} from 'rxjs/Observable';
|
||||||
import {Observer} from 'rxjs/Observer';
|
import {Observer} from 'rxjs/Observer';
|
||||||
import {isSuccess, getResponseURL} from '../http_utils';
|
import {isSuccess, getResponseURL} from '../http_utils';
|
||||||
|
import {ContentType} from '../enums';
|
||||||
|
|
||||||
const XSSI_PREFIX = ')]}\',\n';
|
const XSSI_PREFIX = ')]}\',\n';
|
||||||
|
|
||||||
|
@ -83,6 +84,8 @@ export class XHRConnection implements Connection {
|
||||||
responseObserver.error(new Response(responseOptions));
|
responseObserver.error(new Response(responseOptions));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.setDetectedContentType(req, _xhr);
|
||||||
|
|
||||||
if (isPresent(req.headers)) {
|
if (isPresent(req.headers)) {
|
||||||
req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(',')));
|
req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(',')));
|
||||||
}
|
}
|
||||||
|
@ -90,7 +93,7 @@ export class XHRConnection implements Connection {
|
||||||
_xhr.addEventListener('load', onLoad);
|
_xhr.addEventListener('load', onLoad);
|
||||||
_xhr.addEventListener('error', onError);
|
_xhr.addEventListener('error', onError);
|
||||||
|
|
||||||
_xhr.send(this.request.text());
|
_xhr.send(this.request.getBody());
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
_xhr.removeEventListener('load', onLoad);
|
_xhr.removeEventListener('load', onLoad);
|
||||||
|
@ -99,6 +102,34 @@ export class XHRConnection implements Connection {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDetectedContentType(req, _xhr) {
|
||||||
|
// Skip if a custom Content-Type header is provided
|
||||||
|
if (isPresent(req.headers) && isPresent(req.headers['Content-Type'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the detected content type
|
||||||
|
switch (req.contentType) {
|
||||||
|
case ContentType.NONE:
|
||||||
|
break;
|
||||||
|
case ContentType.JSON:
|
||||||
|
_xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
break;
|
||||||
|
case ContentType.FORM:
|
||||||
|
_xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
||||||
|
break;
|
||||||
|
case ContentType.TEXT:
|
||||||
|
_xhr.setRequestHeader('Content-Type', 'text/plain');
|
||||||
|
break;
|
||||||
|
case ContentType.BLOB:
|
||||||
|
var blob = req.blob();
|
||||||
|
if (blob.type) {
|
||||||
|
_xhr.setRequestHeader('Content-Type', blob.type);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -43,8 +43,7 @@ export class RequestOptions {
|
||||||
/**
|
/**
|
||||||
* Body to be used when creating a {@link Request}.
|
* Body to be used when creating a {@link Request}.
|
||||||
*/
|
*/
|
||||||
// TODO: support FormData, Blob, URLSearchParams
|
body: any;
|
||||||
body: string;
|
|
||||||
/**
|
/**
|
||||||
* Url with which to perform a {@link Request}.
|
* Url with which to perform a {@link Request}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -36,3 +36,16 @@ export enum ResponseType {
|
||||||
Error,
|
Error,
|
||||||
Opaque
|
Opaque
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported content type to be automatically associated with a {@link Request}.
|
||||||
|
*/
|
||||||
|
export enum ContentType {
|
||||||
|
NONE,
|
||||||
|
JSON,
|
||||||
|
FORM,
|
||||||
|
FORM_DATA,
|
||||||
|
TEXT,
|
||||||
|
BLOB,
|
||||||
|
ARRAY_BUFFER
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {RequestOptionsArgs, ConnectionBackend} from './interfaces';
|
||||||
import {Request} from './static_request';
|
import {Request} from './static_request';
|
||||||
import {Response} from './static_response';
|
import {Response} from './static_response';
|
||||||
import {BaseRequestOptions, RequestOptions} from './base_request_options';
|
import {BaseRequestOptions, RequestOptions} from './base_request_options';
|
||||||
|
import {URLSearchParams} from './url_search_params';
|
||||||
import {RequestMethod} from './enums';
|
import {RequestMethod} from './enums';
|
||||||
import {Observable} from 'rxjs/Observable';
|
import {Observable} from 'rxjs/Observable';
|
||||||
|
|
||||||
|
@ -126,7 +127,7 @@ export class Http {
|
||||||
/**
|
/**
|
||||||
* Performs a request with `post` http method.
|
* Performs a request with `post` http method.
|
||||||
*/
|
*/
|
||||||
post(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
|
post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
|
||||||
return httpRequest(
|
return httpRequest(
|
||||||
this._backend,
|
this._backend,
|
||||||
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
||||||
|
@ -136,7 +137,7 @@ export class Http {
|
||||||
/**
|
/**
|
||||||
* Performs a request with `put` http method.
|
* Performs a request with `put` http method.
|
||||||
*/
|
*/
|
||||||
put(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
|
put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
|
||||||
return httpRequest(
|
return httpRequest(
|
||||||
this._backend,
|
this._backend,
|
||||||
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
||||||
|
@ -154,7 +155,7 @@ export class Http {
|
||||||
/**
|
/**
|
||||||
* Performs a request with `patch` http method.
|
* Performs a request with `patch` http method.
|
||||||
*/
|
*/
|
||||||
patch(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
|
patch(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
|
||||||
return httpRequest(
|
return httpRequest(
|
||||||
this._backend,
|
this._backend,
|
||||||
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
||||||
|
|
|
@ -29,8 +29,7 @@ export interface RequestOptionsArgs {
|
||||||
method?: string | RequestMethod;
|
method?: string | RequestMethod;
|
||||||
search?: string | URLSearchParams;
|
search?: string | URLSearchParams;
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
// TODO: Support Blob, ArrayBuffer, JSON, URLSearchParams, FormData
|
body?: any;
|
||||||
body?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {RequestMethod} from './enums';
|
import {RequestMethod} from './enums';
|
||||||
import {RequestArgs} from './interfaces';
|
import {RequestArgs} from './interfaces';
|
||||||
import {Headers} from './headers';
|
import {Headers} from './headers';
|
||||||
|
import {ContentType} from './enums';
|
||||||
|
import {URLSearchParams} from './url_search_params';
|
||||||
import {normalizeMethodName} from './http_utils';
|
import {normalizeMethodName} from './http_utils';
|
||||||
import {isPresent, StringWrapper} from '../src/facade/lang';
|
import {isPresent, StringWrapper} from '../src/facade/lang';
|
||||||
|
|
||||||
|
@ -53,8 +55,10 @@ export class Request {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
/** Url of the remote resource */
|
/** Url of the remote resource */
|
||||||
url: string;
|
url: string;
|
||||||
// TODO: support URLSearchParams | FormData | Blob | ArrayBuffer
|
/** Body of the request **/
|
||||||
private _body: string;
|
private _body: any;
|
||||||
|
/** Type of the request body **/
|
||||||
|
private contentType: ContentType;
|
||||||
constructor(requestOptions: RequestArgs) {
|
constructor(requestOptions: RequestArgs) {
|
||||||
// TODO: assert that url is present
|
// TODO: assert that url is present
|
||||||
let url = requestOptions.url;
|
let url = requestOptions.url;
|
||||||
|
@ -71,6 +75,7 @@ export class Request {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._body = requestOptions.body;
|
this._body = requestOptions.body;
|
||||||
|
this.contentType = this.detectContentType();
|
||||||
this.method = normalizeMethodName(requestOptions.method);
|
this.method = normalizeMethodName(requestOptions.method);
|
||||||
// TODO(jeffbcross): implement behavior
|
// TODO(jeffbcross): implement behavior
|
||||||
// Defaults to 'omit', consistent with browser
|
// Defaults to 'omit', consistent with browser
|
||||||
|
@ -85,4 +90,82 @@ export class Request {
|
||||||
* string.
|
* string.
|
||||||
*/
|
*/
|
||||||
text(): string { return isPresent(this._body) ? this._body.toString() : ''; }
|
text(): string { return isPresent(this._body) ? this._body.toString() : ''; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request's body as JSON string, assuming that body exists. If body is undefined,
|
||||||
|
* return
|
||||||
|
* empty
|
||||||
|
* string.
|
||||||
|
*/
|
||||||
|
json(): string { return isPresent(this._body) ? JSON.stringify(this._body) : ''; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request's body as array buffer, assuming that body exists. If body is undefined,
|
||||||
|
* return
|
||||||
|
* null.
|
||||||
|
*/
|
||||||
|
arrayBuffer(): ArrayBuffer {
|
||||||
|
if (this._body instanceof ArrayBuffer) return <ArrayBuffer>this._body;
|
||||||
|
throw "The request body isn't an array buffer";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request's body as blob, assuming that body exists. If body is undefined, return
|
||||||
|
* null.
|
||||||
|
*/
|
||||||
|
blob(): Blob {
|
||||||
|
if (this._body instanceof Blob) return <Blob>this._body;
|
||||||
|
if (this._body instanceof ArrayBuffer) return new Blob([this._body]);
|
||||||
|
throw "The request body isn't either a blob or an array buffer";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content type of request's body based on its type.
|
||||||
|
*/
|
||||||
|
detectContentType() {
|
||||||
|
if (this._body == null) {
|
||||||
|
return ContentType.NONE;
|
||||||
|
} else if (this._body instanceof URLSearchParams) {
|
||||||
|
return ContentType.FORM;
|
||||||
|
} else if (this._body instanceof FormData) {
|
||||||
|
return ContentType.FORM_DATA;
|
||||||
|
} else if (this._body instanceof Blob) {
|
||||||
|
return ContentType.BLOB;
|
||||||
|
} else if (this._body instanceof ArrayBuffer) {
|
||||||
|
return ContentType.ARRAY_BUFFER;
|
||||||
|
} else if (this._body && typeof this._body == 'object') {
|
||||||
|
return ContentType.JSON;
|
||||||
|
} else {
|
||||||
|
return ContentType.TEXT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request's body according to its type. If body is undefined, return
|
||||||
|
* null.
|
||||||
|
*/
|
||||||
|
getBody(): any {
|
||||||
|
switch (this.contentType) {
|
||||||
|
case ContentType.JSON:
|
||||||
|
return this.json();
|
||||||
|
case ContentType.FORM:
|
||||||
|
return this.text();
|
||||||
|
case ContentType.FORM_DATA:
|
||||||
|
return this._body;
|
||||||
|
case ContentType.TEXT:
|
||||||
|
return this.text();
|
||||||
|
case ContentType.BLOB:
|
||||||
|
return this.blob();
|
||||||
|
case ContentType.ARRAY_BUFFER:
|
||||||
|
return this.arrayBuffer();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const noop = function () {};
|
||||||
|
const w = typeof window == 'object' ? window : noop;
|
||||||
|
const FormData = w['FormData'] || noop;
|
||||||
|
const Blob = w['Blob'] || noop;
|
||||||
|
const ArrayBuffer = w['ArrayBuffer'] || noop;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {Map} from '../../src/facade/collection';
|
||||||
import {RequestOptions, BaseRequestOptions} from '../../src/base_request_options';
|
import {RequestOptions, BaseRequestOptions} from '../../src/base_request_options';
|
||||||
import {BaseResponseOptions, ResponseOptions} from '../../src/base_response_options';
|
import {BaseResponseOptions, ResponseOptions} from '../../src/base_response_options';
|
||||||
import {ResponseType} from '../../src/enums';
|
import {ResponseType} from '../../src/enums';
|
||||||
|
import {URLSearchParams} from '../../src/url_search_params';
|
||||||
|
|
||||||
var abortSpy: any;
|
var abortSpy: any;
|
||||||
var sendSpy: any;
|
var sendSpy: any;
|
||||||
|
@ -176,6 +177,129 @@ export function main() {
|
||||||
expect(setRequestHeaderSpy).toHaveBeenCalledWith('X-Multi', 'a,b');
|
expect(setRequestHeaderSpy).toHaveBeenCalledWith('X-Multi', 'a,b');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use object body and detect content type header to the request', () => {
|
||||||
|
var body = {test: 'val'};
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(JSON.stringify(body));
|
||||||
|
expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use number body and detect content type header to the request', () => {
|
||||||
|
var body = 23;
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith('23');
|
||||||
|
expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use string body and detect content type header to the request', () => {
|
||||||
|
var body = 'some string';
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(body);
|
||||||
|
expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use URLSearchParams body and detect content type header to the request', () => {
|
||||||
|
var body = new URLSearchParams();
|
||||||
|
body.set('test1', 'val1');
|
||||||
|
body.set('test2', 'val2');
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith('test1=val1&test2=val2');
|
||||||
|
expect(setRequestHeaderSpy)
|
||||||
|
.toHaveBeenCalledWith('Content-Type',
|
||||||
|
'application/x-www-form-urlencoded;charset=UTF-8');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (global['Blob']) {
|
||||||
|
it('should use FormData body and detect content type header to the request', () => {
|
||||||
|
var body = new FormData();
|
||||||
|
body.append('test1', 'val1');
|
||||||
|
body.append('test2', 123456);
|
||||||
|
var blob = new Blob(['body { color: red; }'], {type: 'text/css'});
|
||||||
|
body.append("userfile", blob);
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(body);
|
||||||
|
expect(setRequestHeaderSpy).not.toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use blob body and detect content type header to the request', () => {
|
||||||
|
var body = new Blob(['body { color: red; }'], {type: 'text/css'});
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(body);
|
||||||
|
expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use blob body without type to the request', () => {
|
||||||
|
var body = new Blob(['body { color: red; }']);
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(body);
|
||||||
|
expect(setRequestHeaderSpy).not.toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use blob body without type with custom content type header to the request', () => {
|
||||||
|
var headers = new Headers({'Content-Type': 'text/css'});
|
||||||
|
var body = new Blob(['body { color: red; }']);
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body, headers: headers}))),
|
||||||
|
new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(body);
|
||||||
|
expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use array buffer body to the request', () => {
|
||||||
|
var body = new ArrayBuffer(512);
|
||||||
|
var longInt8View = new Uint8Array(body);
|
||||||
|
for (var i = 0; i < longInt8View.length; i++) {
|
||||||
|
longInt8View[i] = i % 255;
|
||||||
|
}
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(body);
|
||||||
|
expect(setRequestHeaderSpy).not.toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use array buffer body without type with custom content type header to the request',
|
||||||
|
() => {
|
||||||
|
var headers = new Headers({'Content-Type': 'text/css'});
|
||||||
|
var body = new ArrayBuffer(512);
|
||||||
|
var longInt8View = new Uint8Array(body);
|
||||||
|
for (var i = 0; i < longInt8View.length; i++) {
|
||||||
|
longInt8View[i] = i % 255;
|
||||||
|
}
|
||||||
|
var base = new BaseRequestOptions();
|
||||||
|
var connection = new XHRConnection(
|
||||||
|
new Request(base.merge(new RequestOptions({body: body, headers: headers}))),
|
||||||
|
new MockBrowserXHR());
|
||||||
|
connection.response.subscribe();
|
||||||
|
expect(sendSpy).toHaveBeenCalledWith(body);
|
||||||
|
expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it('should return the correct status code',
|
it('should return the correct status code',
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||||
var statusCode = 418;
|
var statusCode = 418;
|
||||||
|
|
|
@ -50,6 +50,8 @@ export class BrowserDetection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BrowserDetection.setup();
|
||||||
|
|
||||||
export function dispatchEvent(element, eventType): void {
|
export function dispatchEvent(element, eventType): void {
|
||||||
getDOM().dispatchEvent(element, getDOM().createEvent(eventType));
|
getDOM().dispatchEvent(element, getDOM().createEvent(eventType));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue