Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(http): automatically set request Content-Type header based on body type #7310

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 32 additions & 1 deletion modules/angular2/src/http/backends/xhr_backend.ts
Expand Up @@ -10,6 +10,7 @@ import {isPresent} from 'angular2/src/facade/lang';
import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer';
import {isSuccess, getResponseURL} from '../http_utils';
import {ContentType} from '../enums';

/**
* Creates connections using `XMLHttpRequest`. Given a fully-qualified
Expand Down Expand Up @@ -74,14 +75,16 @@ 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(',')));
}

_xhr.addEventListener('load', onLoad);
_xhr.addEventListener('error', onError);

_xhr.send(this.request.text());
_xhr.send(this.request.getBody());

return () => {
_xhr.removeEventListener('load', onLoad);
Expand All @@ -90,6 +93,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;
}
}
}

/**
Expand Down
3 changes: 1 addition & 2 deletions modules/angular2/src/http/base_request_options.ts
Expand Up @@ -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}.
*/
Expand Down
13 changes: 13 additions & 0 deletions modules/angular2/src/http/enums.ts
Expand Up @@ -38,3 +38,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
}
7 changes: 4 additions & 3 deletions modules/angular2/src/http/http.ts
Expand Up @@ -5,6 +5,7 @@ import {RequestOptionsArgs, Connection, 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';

Expand Down Expand Up @@ -125,7 +126,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})),
Expand All @@ -135,7 +136,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})),
Expand All @@ -153,7 +154,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})),
Expand Down
3 changes: 1 addition & 2 deletions modules/angular2/src/http/interfaces.ts
Expand Up @@ -31,8 +31,7 @@ export interface RequestOptionsArgs {
method?: string | RequestMethod;
search?: string | URLSearchParams;
headers?: Headers;
// TODO: Support Blob, ArrayBuffer, JSON, URLSearchParams, FormData
body?: string;
body?: any;
}

/**
Expand Down
82 changes: 80 additions & 2 deletions modules/angular2/src/http/static_request.ts
@@ -1,12 +1,15 @@
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 {
RegExpWrapper,
CONST_EXPR,
isPresent,
isJsObject,
isPrimitive,
StringWrapper
} from 'angular2/src/facade/lang';

Expand Down Expand Up @@ -59,8 +62,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;
Expand All @@ -77,6 +82,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
Expand All @@ -91,4 +97,76 @@ 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 (isPrimitive(this._body)) {
return ContentType.TEXT;
} else {
return ContentType.JSON;
}
}

/**
* 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;
};
}
}
2 changes: 2 additions & 0 deletions modules/angular2/src/testing/utils.ts
Expand Up @@ -65,6 +65,8 @@ export class BrowserDetection {
}
}

BrowserDetection.setup();

export function dispatchEvent(element, eventType): void {
DOM.dispatchEvent(element, DOM.createEvent(eventType));
}
Expand Down
122 changes: 122 additions & 0 deletions modules/angular2/test/http/backends/xhr_backend_spec.ts
Expand Up @@ -22,6 +22,7 @@ import {Map} from 'angular2/src/facade/collection';
import {RequestOptions, BaseRequestOptions} from 'angular2/src/http/base_request_options';
import {BaseResponseOptions, ResponseOptions} from 'angular2/src/http/base_response_options';
import {ResponseType} from 'angular2/src/http/enums';
import {URLSearchParams} from 'angular2/src/http/url_search_params';

var abortSpy: any;
var sendSpy: any;
Expand Down Expand Up @@ -174,6 +175,127 @@ 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');
});

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;
Expand Down