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.
|
||||
*/
|
||||
export var reflector = new Reflector(new ReflectionCapabilities());
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {isPresent, isString} from '../../src/facade/lang';
|
|||
import {Observable} from 'rxjs/Observable';
|
||||
import {Observer} from 'rxjs/Observer';
|
||||
import {isSuccess, getResponseURL} from '../http_utils';
|
||||
import {ContentType} from '../enums';
|
||||
|
||||
const XSSI_PREFIX = ')]}\',\n';
|
||||
|
||||
|
@ -83,6 +84,8 @@ export class XHRConnection implements Connection {
|
|||
responseObserver.error(new Response(responseOptions));
|
||||
};
|
||||
|
||||
this.setDetectedContentType(req, _xhr);
|
||||
|
||||
if (isPresent(req.headers)) {
|
||||
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('error', onError);
|
||||
|
||||
_xhr.send(this.request.text());
|
||||
_xhr.send(this.request.getBody());
|
||||
|
||||
return () => {
|
||||
_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}.
|
||||
*/
|
||||
// TODO: support FormData, Blob, URLSearchParams
|
||||
body: string;
|
||||
body: any;
|
||||
/**
|
||||
* Url with which to perform a {@link Request}.
|
||||
*/
|
||||
|
|
|
@ -36,3 +36,16 @@ export enum ResponseType {
|
|||
Error,
|
||||
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 {Response} from './static_response';
|
||||
import {BaseRequestOptions, RequestOptions} from './base_request_options';
|
||||
import {URLSearchParams} from './url_search_params';
|
||||
import {RequestMethod} from './enums';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
|
@ -126,7 +127,7 @@ export class Http {
|
|||
/**
|
||||
* 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(
|
||||
this._backend,
|
||||
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
||||
|
@ -136,7 +137,7 @@ export class Http {
|
|||
/**
|
||||
* 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(
|
||||
this._backend,
|
||||
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
||||
|
@ -154,7 +155,7 @@ export class Http {
|
|||
/**
|
||||
* 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(
|
||||
this._backend,
|
||||
new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})),
|
||||
|
|
|
@ -29,8 +29,7 @@ export interface RequestOptionsArgs {
|
|||
method?: string | RequestMethod;
|
||||
search?: string | URLSearchParams;
|
||||
headers?: Headers;
|
||||
// TODO: Support Blob, ArrayBuffer, JSON, URLSearchParams, FormData
|
||||
body?: string;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {RequestMethod} from './enums';
|
||||
import {RequestArgs} from './interfaces';
|
||||
import {Headers} from './headers';
|
||||
import {ContentType} from './enums';
|
||||
import {URLSearchParams} from './url_search_params';
|
||||
import {normalizeMethodName} from './http_utils';
|
||||
import {isPresent, StringWrapper} from '../src/facade/lang';
|
||||
|
||||
|
@ -53,8 +55,10 @@ export class Request {
|
|||
headers: Headers;
|
||||
/** Url of the remote resource */
|
||||
url: string;
|
||||
// TODO: support URLSearchParams | FormData | Blob | ArrayBuffer
|
||||
private _body: string;
|
||||
/** Body of the request **/
|
||||
private _body: any;
|
||||
/** Type of the request body **/
|
||||
private contentType: ContentType;
|
||||
constructor(requestOptions: RequestArgs) {
|
||||
// TODO: assert that url is present
|
||||
let url = requestOptions.url;
|
||||
|
@ -71,6 +75,7 @@ export class Request {
|
|||
}
|
||||
}
|
||||
this._body = requestOptions.body;
|
||||
this.contentType = this.detectContentType();
|
||||
this.method = normalizeMethodName(requestOptions.method);
|
||||
// TODO(jeffbcross): implement behavior
|
||||
// Defaults to 'omit', consistent with browser
|
||||
|
@ -85,4 +90,82 @@ export class Request {
|
|||
* string.
|
||||
*/
|
||||
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 {BaseResponseOptions, ResponseOptions} from '../../src/base_response_options';
|
||||
import {ResponseType} from '../../src/enums';
|
||||
import {URLSearchParams} from '../../src/url_search_params';
|
||||
|
||||
var abortSpy: any;
|
||||
var sendSpy: any;
|
||||
|
@ -176,6 +177,129 @@ export function main() {
|
|||
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',
|
||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||
var statusCode = 418;
|
||||
|
|
|
@ -50,6 +50,8 @@ export class BrowserDetection {
|
|||
}
|
||||
}
|
||||
|
||||
BrowserDetection.setup();
|
||||
|
||||
export function dispatchEvent(element, eventType): void {
|
||||
getDOM().dispatchEvent(element, getDOM().createEvent(eventType));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue