@vex-chat/libvex 6.6.1 → 6.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,8 +32,7 @@ import type { Storage } from "../../Storage.js";
32
32
 
33
33
  import { getCryptoProfile, setCryptoProfile } from "@vex-chat/crypto";
34
34
 
35
- import { isAxiosError } from "axios";
36
-
35
+ import { isHttpError } from "../../http.js";
37
36
  import { Client } from "../../index.js";
38
37
 
39
38
  import { testFile, testImage } from "./fixtures.js";
@@ -394,6 +393,37 @@ export function platformSuite(
394
393
  expect(new Uint8Array(fetched!.data)).toEqual(testFile);
395
394
  });
396
395
 
396
+ test("file upload + download via JSON fallback path", async () => {
397
+ const globalWithFormData = globalThis as typeof globalThis & {
398
+ FormData?: unknown;
399
+ };
400
+ const originalFormData = globalWithFormData.FormData;
401
+ Object.defineProperty(globalThis, "FormData", {
402
+ configurable: true,
403
+ enumerable: true,
404
+ value: undefined,
405
+ writable: true,
406
+ });
407
+ try {
408
+ const [details, key] = await client.files.create(testFile);
409
+ expect(details.fileID).toBeTruthy();
410
+
411
+ const fetched = await client.files.retrieve(
412
+ details.fileID,
413
+ key,
414
+ );
415
+ expect(fetched).toBeTruthy();
416
+ expect(new Uint8Array(fetched!.data)).toEqual(testFile);
417
+ } finally {
418
+ Object.defineProperty(globalThis, "FormData", {
419
+ configurable: true,
420
+ enumerable: true,
421
+ value: originalFormData,
422
+ writable: true,
423
+ });
424
+ }
425
+ });
426
+
397
427
  test("emoji upload", async () => {
398
428
  const server = await client.servers.create("Emoji Test Server");
399
429
  const emoji = await client.emoji.create(
@@ -685,7 +715,7 @@ async function withTransientRetry<T>(fn: () => Promise<T>): Promise<T> {
685
715
  } catch (e) {
686
716
  last = e;
687
717
  const transient =
688
- isAxiosError(e) &&
718
+ isHttpError(e) &&
689
719
  (e.response?.status === 502 || e.response?.status === 503);
690
720
  if (transient && i < attempts - 1) {
691
721
  await new Promise((r) => setTimeout(r, 400 * (i + 1)));
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
7
+ import { afterEach, describe, expect, it } from "vitest";
8
+
9
+ import { createFetchHttpClient, isHttpError } from "../http.js";
10
+
11
+ const originalFetch = globalThis.fetch;
12
+
13
+ afterEach(() => {
14
+ globalThis.fetch = originalFetch;
15
+ });
16
+
17
+ describe("FetchHttpClient", () => {
18
+ it("preserves status metadata for empty JSON error responses", async () => {
19
+ globalThis.fetch = () =>
20
+ Promise.resolve(
21
+ new Response(null, {
22
+ status: 401,
23
+ statusText: "Unauthorized",
24
+ }),
25
+ );
26
+
27
+ const client = createFetchHttpClient();
28
+ const err = await captureError(() =>
29
+ client.post(
30
+ "https://example.test/device/id/notifications",
31
+ {},
32
+ { responseType: "json" },
33
+ ),
34
+ );
35
+
36
+ expect(isHttpError(err)).toBe(true);
37
+ if (!isHttpError(err)) {
38
+ throw err;
39
+ }
40
+ expect(err.response?.status).toBe(401);
41
+ expect(err.response?.statusText).toBe("Unauthorized");
42
+ expect(err.response?.data).toBeNull();
43
+ expect(err.config.method).toBe("POST");
44
+ });
45
+
46
+ it("preserves status metadata for non-JSON error responses", async () => {
47
+ globalThis.fetch = () =>
48
+ Promise.resolve(
49
+ new Response("plain failure", {
50
+ headers: { "Content-Type": "text/plain" },
51
+ status: 400,
52
+ statusText: "Bad Request",
53
+ }),
54
+ );
55
+
56
+ const client = createFetchHttpClient();
57
+ const err = await captureError(() =>
58
+ client.get("https://example.test/status", {
59
+ responseType: "json",
60
+ }),
61
+ );
62
+
63
+ expect(isHttpError(err)).toBe(true);
64
+ if (!isHttpError(err)) {
65
+ throw err;
66
+ }
67
+ expect(err.response?.status).toBe(400);
68
+ expect(err.response?.data).toBe("plain failure");
69
+ });
70
+
71
+ it("emits a final upload progress event for FormData payloads", async () => {
72
+ globalThis.fetch = () =>
73
+ Promise.resolve(new Response(new ArrayBuffer(0), { status: 200 }));
74
+
75
+ const client = createFetchHttpClient();
76
+ const events: { loaded: number; total?: number }[] = [];
77
+ const payload = new FormData();
78
+ payload.set("file", new Blob([new Uint8Array([1, 2, 3])]));
79
+ payload.set("name", "ok");
80
+
81
+ await client.post("https://example.test/file", payload, {
82
+ onUploadProgress: (event) => {
83
+ events.push(event);
84
+ },
85
+ });
86
+
87
+ expect(events).toEqual([{ loaded: 5, total: 5 }]);
88
+ });
89
+ });
90
+
91
+ async function captureError(fn: () => Promise<unknown>): Promise<unknown> {
92
+ try {
93
+ await fn();
94
+ } catch (err: unknown) {
95
+ return err;
96
+ }
97
+ throw new Error("Expected function to throw");
98
+ }
package/src/codec.ts CHANGED
@@ -59,7 +59,7 @@ function msgpackDecode(data: Uint8Array): unknown {
59
59
  /**
60
60
  * Encode a value to msgpack. Returns a fresh Uint8Array copy
61
61
  * (not a subarray of the internal pool buffer) to avoid browser
62
- * XMLHttpRequest.send() corruption (axios issue #4068).
62
+ * transport corruption when a runtime reuses pooled buffers.
63
63
  */
64
64
  function msgpackEncode(value: unknown): Uint8Array {
65
65
  const packed = _packr.encode(value);
package/src/codecs.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * Pre-built codec instances for every HTTP response type.
9
9
  *
10
10
  * Usage: import { UserCodec } from "./codecs.js";
11
- * const data = decodeAxios(UserCodec, res.data);
11
+ * const data = decodeHttpResponse(UserCodec, res.data);
12
12
  *
13
13
  * decode() returns typed data without runtime validation (SDK trusts server).
14
14
  * For trust boundary validation, use codec.decodeSafe() directly.
@@ -190,18 +190,15 @@ export const PasskeyAuthFinishResponseCodec = createCodec(
190
190
  }),
191
191
  );
192
192
 
193
- // ── Helper: decode axios response buffer ────────────────────────────────────
193
+ // ── Helper: decode binary HTTP response buffer ──────────────────────────────
194
194
 
195
195
  /**
196
- * Decode an axios arraybuffer response with a typed codec.
196
+ * Decode a binary HTTP response with a typed codec.
197
197
  * Uses decodeSafe (Zod-validated) so schema mismatches surface immediately.
198
198
  */
199
- export function decodeAxios<T>(
199
+ export function decodeHttpResponse<T>(
200
200
  codec: { decodeSafe: (data: Uint8Array) => T },
201
- /**
202
- * Accepts `unknown` because axios types its `responseType: 'arraybuffer'`
203
- * responses as `any`. At runtime this is always an `ArrayBuffer`.
204
- */
201
+ /** Accepts `unknown` so callers can pass transport response data directly. */
205
202
  data: unknown,
206
203
  ): T {
207
204
  if (data instanceof Uint8Array) {
@@ -210,5 +207,5 @@ export function decodeAxios<T>(
210
207
  if (data instanceof ArrayBuffer) {
211
208
  return codec.decodeSafe(new Uint8Array(data));
212
209
  }
213
- throw new Error("Expected Uint8Array or ArrayBuffer from axios response");
210
+ throw new Error("Expected Uint8Array or ArrayBuffer from HTTP response");
214
211
  }