fix(file-upload): fall back through parallel host fields when uploadIp is empty
User report: `upload_private_file` was failing with "private file upload host is invalid" on a fresh upload (boolFileExist=false). The OIDB call succeeded but `uploadIp` (field 60) came back empty, and we treated that single field as the only source of truth. Cross-referenced LagrangeGo's `OidbSvcTrpcTcp0xE37_800.proto` + NapCat's `Oidb.0XE37_800.ts`: the server has six parallel host slots on this response (uploadIp / uploadDomain / uploadIpList[0] / uploadHttpsDomain / uploadDns / uploadLanip). LagrangeGo + Lagrange.Core only read field 60 and would hit the same dead-end — we walk the chain instead. Also log the full slot set when we genuinely can't find a host so future reports can name which field the server actually populated. Adds two regression tests: one proves the fallback to `uploadDomain` goes to highway successfully, one proves we still throw when every slot is empty.
This commit is contained in:
@@ -440,10 +440,42 @@ export async function uploadPrivateFile(
|
||||
if (!fileId) throw new Error('private file upload response missing file_id');
|
||||
|
||||
if (!upload.boolFileExist && uploadFile) {
|
||||
// The server normally returns the highway host in `uploadIp` (field
|
||||
// 60). LagrangeGo / Lagrange.Core both read that field directly with
|
||||
// no fallback. We've seen real captures where the server picks one
|
||||
// of the parallel host fields (uploadDomain / uploadIpList[0] /
|
||||
// uploadHttpsDomain / uploadDns) instead — likely a rollout
|
||||
// difference. Walk them in order rather than die with "upload host
|
||||
// is invalid" when the server gives us a perfectly usable host
|
||||
// through a different slot. The highway client uses HTTP PUT so we
|
||||
// can pair any of these with the regular `uploadPort`; we only fall
|
||||
// back to `uploadHttpsPort` if we picked the HTTPS-flavored host.
|
||||
const ipListFirst = (Array.isArray(upload.uploadIpList) && upload.uploadIpList[0])
|
||||
? upload.uploadIpList[0] : '';
|
||||
const uploadHost = (typeof upload.uploadIp === 'string' && upload.uploadIp)
|
||||
|| (typeof upload.uploadDomain === 'string' && upload.uploadDomain)
|
||||
|| (ipListFirst)
|
||||
|| (typeof upload.uploadHttpsDomain === 'string' && upload.uploadHttpsDomain)
|
||||
|| (typeof upload.uploadDns === 'string' && upload.uploadDns)
|
||||
|| '';
|
||||
const uploadPort = toInt(upload.uploadPort);
|
||||
const httpsHostUsed = !upload.uploadIp && !upload.uploadDomain && !ipListFirst
|
||||
&& typeof upload.uploadHttpsDomain === 'string' && !!upload.uploadHttpsDomain;
|
||||
const uploadPort = httpsHostUsed && toInt(upload.uploadHttpsPort) > 0
|
||||
? toInt(upload.uploadHttpsPort)
|
||||
: toInt(upload.uploadPort);
|
||||
if (!uploadHost || uploadPort <= 0) {
|
||||
// Surface every host-bearing field we know about so a user
|
||||
// hitting this can show us exactly which slot the server filled
|
||||
// (or whether it returned nothing at all — which would point at
|
||||
// a request mismatch rather than a missing decoder).
|
||||
log.warn(
|
||||
'private file upload host missing — ip=%s domain=%s ipList=%s httpsDomain=%s dns=%s lanip=%s port=%s httpsPort=%s',
|
||||
upload.uploadIp ?? '', upload.uploadDomain ?? '',
|
||||
JSON.stringify(upload.uploadIpList ?? []),
|
||||
upload.uploadHttpsDomain ?? '', upload.uploadDns ?? '',
|
||||
upload.uploadLanip ?? '', upload.uploadPort ?? 0,
|
||||
upload.uploadHttpsPort ?? 0,
|
||||
);
|
||||
throw new Error('private file upload host is invalid');
|
||||
}
|
||||
|
||||
|
||||
@@ -646,11 +646,27 @@ export interface OidbPrivateFileUploadReq {
|
||||
export interface OidbPrivateFileUploadRespBody {
|
||||
retCode?: pb<10, int_32>;
|
||||
retMsg?: pb<20, string>;
|
||||
// 30/40/50 (totalSpace/usedSpace/uploadedSize int64) intentionally
|
||||
// omitted — proton only needs fields we actually consume, and the
|
||||
// quota numbers don't drive any code path.
|
||||
uploadIp?: pb<60, string>;
|
||||
// Cross-checked against LagrangeGo `OidbSvcTrpcTcp0xE37_800.proto`
|
||||
// (fields 60-170 enumerated below) + NapCat `Oidb.0XE37_800.ts`.
|
||||
// Real server has been observed to leave `uploadIp` empty while
|
||||
// populating `uploadDomain` / `uploadIpList[0]` instead (likely a
|
||||
// server-side rollout difference). Decoding all of these lets us
|
||||
// fall through in the consumer rather than die with "upload host
|
||||
// is invalid" when the server picks a different field.
|
||||
uploadDomain?: pb<70, string>;
|
||||
uploadPort?: pb<80, uint_32>;
|
||||
uuid?: pb<90, string>;
|
||||
uploadKey?: pb<100, bytes>;
|
||||
boolFileExist?: pb<110, bool>;
|
||||
uploadIpList?: pb_repeated<130, string>;
|
||||
uploadHttpsPort?: pb<140, int_32>;
|
||||
uploadHttpsDomain?: pb<150, string>;
|
||||
uploadDns?: pb<160, string>;
|
||||
uploadLanip?: pb<170, string>;
|
||||
fileAddon?: pb<200, string>;
|
||||
mediaPlatformUploadKey?: pb<220, bytes>;
|
||||
}
|
||||
|
||||
@@ -242,6 +242,51 @@ describe('actions/group-file', () => {
|
||||
expect(bridge.sendC2cFileMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uploadPrivateFile falls back to uploadDomain when uploadIp is empty', async () => {
|
||||
// Regression: the server has been observed in the wild returning the
|
||||
// highway host in `uploadDomain` (field 70) instead of `uploadIp`
|
||||
// (field 60), causing "private file upload host is invalid". The
|
||||
// helper should walk the parallel host fields rather than dying on
|
||||
// a single missing slot. Cross-referenced against napcat's proto
|
||||
// (`Oidb.0XE37_800.ts` ApplyUploadRespV3) — fields 60/70/130/150/160
|
||||
// all carry plausible host values from the same server endpoint.
|
||||
const bridge = mockBridge();
|
||||
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
|
||||
vi.mocked(oidb.runOidb).mockResolvedValueOnce(
|
||||
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
|
||||
body: {
|
||||
upload: {
|
||||
uuid: 'pfid',
|
||||
fileAddon: 'phash',
|
||||
// boolFileExist defaults to false (proto3) — forces the
|
||||
// highway path where the host fields actually matter.
|
||||
uploadDomain: 'upload.qpic.cn',
|
||||
uploadPort: 8080,
|
||||
mediaPlatformUploadKey: new Uint8Array([1, 2, 3]),
|
||||
} as any,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await groupFile.uploadPrivateFile(bridge as any, 67890, '/path/file.bin');
|
||||
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('uploadPrivateFile throws "host is invalid" only when every host field is empty', async () => {
|
||||
// The diagnostic warn-log lists every host slot to help users
|
||||
// report which one the server actually populated; this test just
|
||||
// asserts that the throw still fires when none of them carry a
|
||||
// value (boolFileExist=false + no usable host).
|
||||
const bridge = mockBridge();
|
||||
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
|
||||
vi.mocked(oidb.runOidb).mockResolvedValueOnce(
|
||||
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
|
||||
body: { upload: { uuid: 'pfid', fileAddon: 'phash', uploadPort: 8080 } as any },
|
||||
}),
|
||||
);
|
||||
await expect(groupFile.uploadPrivateFile(bridge as any, 67890, '/path/file.bin'))
|
||||
.rejects.toThrow(/upload host is invalid/);
|
||||
});
|
||||
|
||||
it('fetchGroupFiles paginates files + folders out of OIDB items', async () => {
|
||||
const bridge = mockBridge();
|
||||
vi.mocked(oidb.runOidb).mockResolvedValueOnce(
|
||||
|
||||
Reference in New Issue
Block a user