Files
im/uni_modules/network-manage/utssdk/app-harmony/network/request.uts
T
2025-12-27 07:08:30 +08:00

361 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<number, RequestTaskOnChunkReceivedCallback> = new Map()
private _requestOnHeadersReceiveCallbackId: number = 0
private _requestOnHeadersReceiveCallbacks: Map<number, RequestTaskOnHeadersReceivedCallback> = 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<UniRequestOptions<Object>, UniRequestSuccess<Object>, UniRequestTask>(
API_REQUEST,
(args: UniRequestOptions<Object>, exec: ApiExecutor<UniRequestSuccess<Object>>) => {
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<string, Object>
const headerRecord = header as Object as Record<string, string>
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<string, Object>
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<string, Object>
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<string, string | string[]>
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<string, string | string[]>
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方法第二个参数可以传errCodereject(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<string, string | string[]>
// 有用户反馈部分情况下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<Object>)
}
realRequestTask.offChunkReceived()
realRequestTask.offHeadersReceived()
httpRequest.destroy() // 调用完毕后必须调用destroy方法
mp.off('beforeClose', destroy)
}
)
return realRequestTask
},
RequestApiProtocol,
RequestApiOptions
) as Request<Object>