mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 23:57:15 +00:00
fix(frontend): preserve cancellation and reject invalid query data (#6143)
* fix(frontend): preserve request cancellation and schema failures * fix(frontend): limit schema failures to query boundaries * fix(frontend): keep invalid settings recoverable Keep settings payload validation tolerant so values accepted by the backend remain editable, while paged clients still fail closed. Add an AbortSignal.any fallback and make timeout tests event-driven. --------- Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
This commit is contained in:
@@ -193,4 +193,87 @@ describe('http-init fetch wrapper', () => {
|
||||
|
||||
expect(initOf().signal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
|
||||
it('preserves a caller cancellation signal when a timeout is set', async () => {
|
||||
http.setupHttp();
|
||||
fetchMock.mockResolvedValue(okEnvelope());
|
||||
const controller = new AbortController();
|
||||
|
||||
await http.httpRequest('GET', '/x', undefined, { timeout: 1_000, signal: controller.signal });
|
||||
controller.abort();
|
||||
|
||||
expect(initOf().signal?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves both cancellation paths when AbortSignal.any is unavailable', async () => {
|
||||
const timeout = AbortSignal.timeout.bind(AbortSignal);
|
||||
vi.resetModules();
|
||||
vi.stubGlobal('AbortSignal', { timeout });
|
||||
http = await import('@/api/http-init');
|
||||
http.setupHttp();
|
||||
fetchMock.mockResolvedValue(okEnvelope());
|
||||
const controller = new AbortController();
|
||||
|
||||
await http.httpRequest('GET', '/x', undefined, { timeout: 1_000, signal: controller.signal });
|
||||
controller.abort();
|
||||
|
||||
expect(initOf().signal?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it('times out when AbortSignal.any is unavailable', async () => {
|
||||
const timeout = AbortSignal.timeout.bind(AbortSignal);
|
||||
vi.resetModules();
|
||||
vi.stubGlobal('AbortSignal', { timeout });
|
||||
http = await import('@/api/http-init');
|
||||
http.setupHttp();
|
||||
fetchMock.mockResolvedValue(okEnvelope());
|
||||
const controller = new AbortController();
|
||||
|
||||
await http.httpRequest('GET', '/x', undefined, { timeout: 20, signal: controller.signal });
|
||||
const signal = initOf().signal as AbortSignal;
|
||||
await new Promise<void>((resolve) => signal.addEventListener('abort', () => resolve(), { once: true }));
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it('aborts on the timeout when a caller signal is present', async () => {
|
||||
http.setupHttp();
|
||||
fetchMock.mockResolvedValue(okEnvelope());
|
||||
const controller = new AbortController();
|
||||
|
||||
await http.httpRequest('GET', '/x', undefined, { timeout: 20, signal: controller.signal });
|
||||
const signal = initOf().signal as AbortSignal;
|
||||
await new Promise<void>((resolve) => {
|
||||
if (signal.aborted) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
signal.addEventListener('abort', () => resolve(), { once: true });
|
||||
});
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['/x?keep=1', '/x?keep=1&added=yes'],
|
||||
['/x?', '/x?added=yes'],
|
||||
['/x#frag', '/x?added=yes#frag'],
|
||||
['/x?keep=1#frag', '/x?keep=1&added=yes#frag'],
|
||||
])('appends encoded params to %s', async (url, expected) => {
|
||||
http.setupHttp();
|
||||
fetchMock.mockResolvedValue(okEnvelope());
|
||||
|
||||
await http.httpRequest('GET', url, undefined, { params: { added: 'yes' } });
|
||||
|
||||
expect(urlOf()).toBe(expected);
|
||||
});
|
||||
|
||||
it('preserves the URL when no params are supplied', async () => {
|
||||
http.setupHttp();
|
||||
fetchMock.mockResolvedValue(okEnvelope());
|
||||
|
||||
await http.httpRequest('GET', '/x?keep=1#frag');
|
||||
|
||||
expect(urlOf()).toBe('/x?keep=1#frag');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user