import 'rxjs/add/observable/throw' ; import 'rxjs/add/observable/dom/ajax' ; import 'rxjs/add/observable/empty' ; import 'rxjs/add/operator/catch' ; import 'rxjs/add/operator/map' ; import 'rxjs/add/operator/publishReplay' ; import { AjaxError } from 'rxjs/observable/dom/AjaxObservable' ; import { Observable } from 'rxjs/Observable' ; import { Observer } from 'rxjs/Observer' ; import { Subject } from 'rxjs/Subject' ; import { testable } from '../testable' ; import { forEach , isNonNullable , parseHeaders } from '../utils' ; import { Perf } from '../utils/perf' ; export type AllowedHttpMethod = 'get' | 'post' | 'put' | 'delete' ; export interface HttpErrorMessage { method : AllowedHttpMethod ; url : string ; error : Response ; body ? : any ; [ userDefinedKey ] : any ; } export interface HttpResponseWithHeaders < T = any > { headers : Headers ; body : T ; } export type MethodParams = { url : string ; body ? : any ; _opts : any ; includeHeaders : boolean ; } ; const rxAjaxDefaultHeaderKey2NormKey = { 'X-Requested-With' : 'x-requested-with' , 'Content-Type' : 'content-type' } ; /**
 * Observable.ajax 目前的实现，对请求头字段的已有设置检查没有遵循
 * 头字段 key 不区分大小写的原则，比如：如果用户已經设置 `content-type`，
 * Observable.ajax 内部会发现 `Content-Type` 没有设置，结果会
 * 额外添加一个 `Content-Type` 字段，结果导致浏览器发现请求头字段里
 * 既有 `content-type` 又有 `Content-Type`，出现问题。
 */ const coverRxAjaxHeadersBug = ( normHeaders ) => { forEach ( rxAjaxDefaultHeaderKey2NormKey , ( val , key ) => { if ( isNonNullable ( normHeaders [ val ] ) ) { normHeaders [ key ] = normHeaders [ val ] ; delete normHeaders [ val ] ; } } ) ; } ; /**
 * 检查 respHeaders 里是否有 x-http-status 字段可用，如果有，
 * 优先使用其值作为上层应用需要的 status。
 *
 * 注意：会对传入的 resp 参数有副作用，可能会改变其内的
 * status 字段对应值。
 */ const normResponseStatus = ( resp , respHeaders ) => { const realStatusCode = Number ( respHeaders . get ( 'x-http-status' ) || resp . status ) ; if ( realStatusCode >= 400 && realStatusCode !== resp . status ) { Object . defineProperty ( resp , 'status' , { value : realStatusCode } ) ; } } ; export const HttpError$ = new Subject < HttpErrorMessage > ( ) as any as Observable < HttpErrorMessage > ; export const createMethod = ( method ) : CreateHttpMethod => ( params ) => { const { url , body , _opts , includeHeaders } = params ; if ( testable . UseXMLHTTPRequest && typeof window !== 'undefined' ) { coverRxAjaxHeadersBug ( _opts . headers ) ; const measureReq = Perf . time ( { url , method } ) ; return Observable . ajax ( { url , body , method , headers : _opts . headers , withCredentials : _opts . credentials === 'include' , responseType : _opts . responseType || 'json' , crossDomain : typeof _opts . crossDomain !== 'undefined' ? ! ! _opts . crossDomain : true } ) . map ( value => { const respBody = value . response ; const respXHR = value . xhr ; const respHeaders = parseHeaders ( respXHR . getAllResponseHeaders ( ) ) ; normResponseStatus ( respXHR , respHeaders ) ; if ( respXHR . status >= 400 ) { throw new AjaxError ( `ajax error ${ respXHR . status }` , respXHR , value . request ) ; } if ( ! includeHeaders ) { return respBody ; } return { headers : respHeaders , body : respBody } ; } ) . do ( { next ( ) { measureReq ( { success : true } ) ; } , error ( ) { measureReq ( { success : false } ) ; } } ) . catch ( ( e ) => { const headers = e . xhr . getAllResponseHeaders ( ) ; const response = e . response ; const errorResponse = new Response ( new Blob ( [ JSON . stringify ( response ) ] ) , { status : e . xhr . status , statusText : e . xhr . statusText , headers : headers . length ? parseHeaders ( headers ) : new Headers ( ) } ) ; return Observable . throw ( { method , url , body , error : errorResponse } ) ; } ) ; } else { return Observable . create ( ( observer ) => { const _options = { ... _opts , method : method } ; if ( body ) { _options . body = body ; } let headers ; fetch ( url , _options ) . then ( ( response ) : Promise < string > => { normResponseStatus ( response , response . headers ) ; if ( response . status >= 200 && response . status < 400 ) { headers = response . headers ; return response . text ( ) ; } else { throw response ; } } ) . then ( respText => { let result ; try { const respBody = JSON . parse ( respText ) ; result = ! includeHeaders ? respBody : { headers , body : respBody } ; } catch ( e ) { result = respText ; } observer . next ( result ) ; observer . complete ( ) ; } ) . catch ( ( errorResponse ) => { observer . error ( { method , url , body , error : errorResponse } ) ; } ) ; } ) ; } } ; export const getHttpWithResponseHeaders = < T > ( url ? , errorAdapter$ ? ) : Http < HttpResponseWithHeaders < T > > => { return new Http < HttpResponseWithHeaders < T > > ( url , errorAdapter$ , true ) ; } ; type CreateHttpMethod = ( params : MethodParams ) => Observable < any > ; interface SetHttpMethodsOptions { get : CreateHttpMethod ; put : CreateHttpMethod ; post : CreateHttpMethod ; delete : CreateHttpMethod ; } export class Http < T > { private errorAdapter$ : Subject < HttpErrorMessage > ; private cloned = false ; private request : Observable < T > | undefined ; public mapFn : ( v$ : Observable < T > ) => Observable < any > = ( dist$ => dist$ ) ; private static get = createMethod ( 'get' ) ; private static put = createMethod ( 'put' ) ; private static post = createMethod ( 'post' ) ; private static delete = createMethod ( 'delete' ) ; public static setMethods ( options ) { Http . get = options . get ; Http . put = options . put ; Http . post = options . post ; Http . delete = options . delete ; } private static defaultOpts = ( ) => ( { headers : { } , credentials : 'include' } ) ; constructor ( private url = '' , errorAdapter$ ? , private readonly includeHeaders = false ) { if ( errorAdapter$ ) { this . errorAdapter$ = errorAdapter$ ; } else { this . errorAdapter$ = HttpError$ as Subject < HttpErrorMessage > ; } } private _opts : any = Http . defaultOpts ( ) ; setUrl ( url ) { this . url = url ; return this ; } setHeaders ( headers ) { this . _opts . headers = { ... this . _opts . headers , ... headers } ; return this ; } setToken ( token ) { delete this . _opts . credentials ; this . _opts . headers . Authorization = `OAuth2 ${ token }` ; return this ; } setOpts ( opts ) { this . _opts = { ... this . _opts , ... opts } ; return this ; } restore ( ) { this . _opts = Http . defaultOpts ( ) ; return this ; } get ( ) { this . request = Http . get ( this . params ( ) ) ; return this ; } post ( body ? ) { this . request = Http . post ( { ... this . params ( ) , body } ) ; return this ; } put ( body ? ) { this . request = Http . put ( { ... this . params ( ) , body } ) ; return this ; } delete ( body ? ) { this . request = Http . delete ( { ... this . params ( ) , body } ) ; return this ; } send ( ) { if ( ! this . request ) { return Observable . empty < T > ( ) ; } const response$ = this . mapFn ( this . request ) ; return response$ . catch ( ( msg ) => { if ( msg . error && msg . error . clone ) { const msgClone = { ... msg , error : msg . error . clone ( ) } ; setTimeout ( ( ) => { this . errorAdapter$ . next ( msgClone ) ; } , 10 ) ; } return Observable . throw ( msg ) ; } ) ; } clone ( ) { const result = new Http < T > ( this . url , this . errorAdapter$ ) ; if ( ! this . cloned && this . request ) { this . request = this . request . publishReplay ( 1 ) . refCount ( ) ; this . cloned = true ; result . cloned = true ; } result . request = this . request ; result . mapFn = this . mapFn ; return result ; } private params = ( ) : MethodParams => ( { url : this . url , _opts : this . _opts , includeHeaders : this . includeHeaders } ) ; }