Files
im/uni_modules/network-manage/utssdk/app-harmony/network/request.uts
T

361 lines
15 KiB
Plaintext
Raw Normal View History

2025-12-27 07:08:30 +08:00
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>