import http from '@ohos.net.http' import buffer from '@ohos.buffer'; import { Emitter, getCurrentMP, } from '@dcloudio/uni-runtime' import { RequestTask as UniRequestTask, RequestOptions as UniRequestOptions, RequestSuccess as UniRequestSuccess, Request, RequestTaskOnChunkReceivedCallback, RequestTaskOnChunkReceivedListenerResult, RequestTaskOnHeadersReceivedCallback, RequestTaskOnHeadersReceivedListenerResult, } from '../../interface.uts' import { API_REQUEST, RequestApiOptions, RequestApiProtocol, } from '../../protocol.uts' import { getClientCertificate, parseUrl, } from './utils.uts' import { getCookieSync, setCookieSync, } from './cookie.uts' interface IUniRequestEmitter { on: (eventName: string, callback: Function) => void once: (eventName: string, callback: Function) => void off: (eventName: string, callback?: Function | null) => void emit: (eventName: string, ...args: (Object | undefined | null)[]) => void } interface IRequestTask { abort: Function onChunkReceived: (listener: RequestTaskOnChunkReceivedCallback) => void offChunkReceived: (listener?: RequestTaskOnChunkReceivedCallback | null) => void onHeadersReceived: (listener: RequestTaskOnHeadersReceivedCallback) => void offHeadersReceived: (listener?: RequestTaskOnHeadersReceivedCallback | null) => void } class RequestTask implements UniRequestTask { __v_skip: boolean = true private _requestTask: IRequestTask private _requestOnChunkReceiveCallbackId: number = 0 private _requestOnChunkReceiveCallbacks: Map = new Map() private _requestOnHeadersReceiveCallbackId: number = 0 private _requestOnHeadersReceiveCallbacks: Map = new Map() constructor(requestTask: IRequestTask) { this._requestTask = requestTask } abort() { this._requestTask.abort() } onChunkReceived(callback: RequestTaskOnChunkReceivedCallback) { this._requestTask.onChunkReceived(callback) this._requestOnChunkReceiveCallbackId++ this._requestOnChunkReceiveCallbacks.set(this._requestOnChunkReceiveCallbackId, callback) return this._requestOnChunkReceiveCallbackId } offChunkReceived(callbackId?: RequestTaskOnChunkReceivedCallback | number | null) { if (callbackId === undefined || callbackId === null) { this._requestTask.offChunkReceived() this._requestOnChunkReceiveCallbacks.clear() return } if (typeof callbackId === 'function') { this._requestOnChunkReceiveCallbacks.forEach((callback, id) => { if (callback === callbackId) { this._requestOnChunkReceiveCallbacks.delete(id) this._requestTask.offChunkReceived(callback) return } }) return } const callback = this._requestOnChunkReceiveCallbacks.get(callbackId) if (!callback) { return } this._requestOnChunkReceiveCallbacks.delete(callbackId) this._requestTask.offChunkReceived(callback) } onHeadersReceived(callback: RequestTaskOnHeadersReceivedCallback) { this._requestTask.onHeadersReceived(callback) this._requestOnHeadersReceiveCallbackId++ this._requestOnHeadersReceiveCallbacks.set(this._requestOnHeadersReceiveCallbackId, callback) return this._requestOnHeadersReceiveCallbackId } offHeadersReceived(callbackId?: RequestTaskOnHeadersReceivedCallback | number | null) { if (callbackId === undefined || callbackId === null) { this._requestTask.offHeadersReceived() this._requestOnHeadersReceiveCallbacks.clear() return } if (typeof callbackId === 'function') { this._requestOnHeadersReceiveCallbacks.forEach((callback, id) => { if (callback === callbackId) { this._requestOnHeadersReceiveCallbacks.delete(id) this._requestTask.offHeadersReceived(callback) return } }) return } const callback = this._requestOnHeadersReceiveCallbacks.get(callbackId) if (!callback) { return } this._requestOnHeadersReceiveCallbacks.delete(callbackId) this._requestTask.offHeadersReceived(callback) } } /** * http.request有5MB响应体大小限制 * rcp.Session.fetch无响应体大小限制,但是headers、cookies处理不够完善,另外rcp需要从@hms引用,不是openharmony标准库 * 为突破http.request的限制,现在使用http.requestInStream */ export const request = defineTaskApi, UniRequestSuccess, UniRequestTask>( API_REQUEST, (args: UniRequestOptions, exec: ApiExecutor>) => { let { header, method, dataType, timeout, url, responseType } = args let data: ESObject = args.data header = header || {} as ESObject if (!header!['Cookie'] && !header!['cookie']) { header!['Cookie'] = getCookieSync(url); } let contentType = '' // header const headers = {} as Record const headerRecord = header as Object as Record const headerKeys = Object.keys(headerRecord) for (let i = 0; i < headerKeys.length; i++) { const name = headerKeys[i]; if (name.toLowerCase() === 'content-type') { contentType = headerRecord[name] as string } headers[name.toLowerCase()] = headerRecord[name] } if (!contentType && method === 'POST') { headers['Content-Type'] = 'application/json' contentType = 'application/json' } // url data if (method === 'GET' && data && typeof data === 'object') { const dataRecord = data as Record const query = Object.keys(dataRecord) .map((key) => { return ( encodeURIComponent(key) + '=' + encodeURIComponent(dataRecord[key] as string | number | boolean) ) }) .join('&') url += query ? (url.indexOf('?') > -1 ? '&' : '?') + query : '' data = null } else if ( method !== 'GET' && contentType && contentType.indexOf('application/json') === 0 && data && typeof data !== 'string' ) { data = JSON.stringify(data) } else if ( method !== 'GET' && contentType && contentType.indexOf('application/x-www-form-urlencoded') === 0 && data && typeof data === 'object' ) { const dataRecord = data as Record data = Object.keys(dataRecord) .map((key) => { return ( encodeURIComponent(key) + '=' + encodeURIComponent(dataRecord[key] as number | string | boolean) ) }) .join('&') } const httpRequest = http.createHttp() const mp = getCurrentMP() const userAgent = mp.userAgent.fullUserAgent if (userAgent && headers && !headers!['User-Agent'] && !headers!['user-agent']) { headers!['User-Agent'] = userAgent } const emitter = new Emitter() as IUniRequestEmitter const requestTask: IRequestTask = { abort() { emitter.off('headersReceive') httpRequest.destroy() }, onHeadersReceived(callback: RequestTaskOnHeadersReceivedCallback) { emitter.on('headersReceive', callback) }, offHeadersReceived(callback?: RequestTaskOnHeadersReceivedCallback | null) { emitter.off('headersReceive', callback || undefined) }, onChunkReceived(callback: RequestTaskOnChunkReceivedCallback) { emitter.on('dataReceive', callback) }, offChunkReceived(callback?: RequestTaskOnChunkReceivedCallback | null) { emitter.off('dataReceive', callback || undefined) }, } const realRequestTask = new RequestTask(requestTask) function destroy() { emitter.off('headersReceive') httpRequest.destroy() } mp.on('beforeClose', destroy) let latestHeaders: Object | null = null let lastUrl = url httpRequest.on('headersReceive', (headers: Object) => { const realHeaders = headers as Record const setCookieHeader = realHeaders['set-cookie'] || realHeaders['Set-Cookie'] if (setCookieHeader) { setCookieSync(lastUrl, setCookieHeader as string[]) } latestHeaders = headers const location = realHeaders['location'] || realHeaders['Location'] if (location) { lastUrl = location as string } // TODO headersReceive存在bug,暂不支持回调给用户。 // emitter.emit('headersReceive', headers); }) let headersReceiveTriggered = false const bufs = [] as buffer.Buffer[] httpRequest.on('dataReceive', (data) => { if (!headersReceiveTriggered) { headersReceiveTriggered = true // 注意重定向时会多次触发,但是只需要给用户回调最后一次headers const headers = {} as UTSJSONObject const realHeaders = latestHeaders as Record let cookies = [] as string[] // 有用户反馈部分情况下headers会为null,无法复现,增加判断逻辑容错 if (realHeaders) { const keys = Object.keys(realHeaders) for (let i = 0; i < keys.length; i++) { const key = keys[i] if (key === 'set-cookie' || key === 'Set-Cookie') { const setCookieHeader = realHeaders[key] as string[]; // TODO 待确认为什么鸿蒙会返回重复的Set-Cookie,不合常理。使用 https://httpbin.org/cookies/set/nnn/123 这个接口测试 for (let i = setCookieHeader.length - 1; i >= 0; i--) { const cookie = setCookieHeader[i] if (setCookieHeader.indexOf(cookie) !== i) { setCookieHeader.splice(i, 1) } } cookies = setCookieHeader as string[]; headers[key] = Array.isArray(setCookieHeader) ? setCookieHeader.join(',') : setCookieHeader; } else { headers[key] = realHeaders[key]; } } } emitter.emit('headersReceive', { header: headers, // 鸿蒙不支持在headersReceive时机获取到状态码 statusCode: NaN, cookies: cookies, } as RequestTaskOnHeadersReceivedListenerResult); } bufs.push(buffer.from(data)) if (args.enableChunked) { emitter.emit('dataReceive', { data: data } as RequestTaskOnChunkReceivedListenerResult) } }) httpRequest.requestInStream( parseUrl(url), { header: headers, method: (method || 'GET').toUpperCase() as http.RequestMethod, // 仅OPTIONS不支持 extraData: data || undefined, // 传空字符串会报错 connectTimeout: timeout ? timeout : undefined, // 不支持仅设置一个timeout readTimeout: timeout ? timeout : undefined, clientCert: getClientCertificate(url) } as http.HttpRequestOptions, (err, statusCode) => { if (err) { /** * TODO abort后此处收到如下错误,待确认是否直接将此错误码转为abort错误 * {"code":2300023,"message":"Failed writing received data to disk/application"} * * reject方法第二个参数可以传errCode,reject(err.message, { errCode: -1 }) */ exec.reject(err.message) } else { const responseData = buffer.concat(bufs) let data: ArrayBuffer | string | object = '' if (responseType === 'arraybuffer') { data = responseData.buffer } else { data = responseData.toString('utf8') if (dataType === 'json') { try { // #ifdef UNI-APP-X data = globalThis.UTS.JSON.parse(data) || data; // #endif // #ifndef UNI-APP-X data = JSON.parse(data as string); // #endif } catch (e) { // 与微信保持一致,不抛出异常 } } } const headers = latestHeaders as Record // 有用户反馈部分情况下headers会为null,无法复现,增加判断逻辑容错 const oldCookies = headers ? (headers['Set-Cookie'] || headers['set-cookie'] || []) as string[] : [] as string[] let newCookies = oldCookies.join(',') if (newCookies) { if (headers['Set-Cookie']) { headers['Set-Cookie'] = newCookies } else { headers['set-cookie'] = newCookies } } exec.resolve({ data, statusCode, header: latestHeaders!, cookies: oldCookies, } as UniRequestSuccess) } realRequestTask.offChunkReceived() realRequestTask.offHeadersReceived() httpRequest.destroy() // 调用完毕后必须调用destroy方法 mp.off('beforeClose', destroy) } ) return realRequestTask }, RequestApiProtocol, RequestApiOptions ) as Request