@atproto/xrpc-server 0.11.5 → 0.11.6
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.
- package/CHANGELOG.md +20 -0
- package/package.json +26 -21
- package/jest.config.cjs +0 -21
- package/src/auth.ts +0 -235
- package/src/errors.ts +0 -312
- package/src/index.ts +0 -14
- package/src/logger.ts +0 -8
- package/src/rate-limiter-http.ts +0 -82
- package/src/rate-limiter.ts +0 -279
- package/src/server.ts +0 -858
- package/src/stream/frames.ts +0 -125
- package/src/stream/index.ts +0 -5
- package/src/stream/logger.ts +0 -6
- package/src/stream/server.ts +0 -66
- package/src/stream/stream.ts +0 -39
- package/src/stream/subscription.ts +0 -96
- package/src/stream/types.ts +0 -27
- package/src/types.ts +0 -330
- package/src/util.ts +0 -708
- package/tests/_util.ts +0 -124
- package/tests/auth.test.ts +0 -333
- package/tests/bodies.test.ts +0 -608
- package/tests/errors.test.ts +0 -299
- package/tests/frames.test.ts +0 -135
- package/tests/ipld.test.ts +0 -97
- package/tests/parameters.test.ts +0 -331
- package/tests/parsing.test.ts +0 -89
- package/tests/procedures.test.ts +0 -176
- package/tests/queries.test.ts +0 -140
- package/tests/rate-limiter.test.ts +0 -312
- package/tests/responses.test.ts +0 -72
- package/tests/stream.test.ts +0 -169
- package/tests/subscriptions.test.ts +0 -398
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
package/tests/bodies.test.ts
DELETED
|
@@ -1,608 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert'
|
|
2
|
-
import * as http from 'node:http'
|
|
3
|
-
import { AddressInfo } from 'node:net'
|
|
4
|
-
import { Readable } from 'node:stream'
|
|
5
|
-
import { brotliCompressSync, deflateSync, gzipSync } from 'node:zlib'
|
|
6
|
-
import { jest } from '@jest/globals'
|
|
7
|
-
import { cidForCbor } from '@atproto/common'
|
|
8
|
-
import { randomBytes } from '@atproto/crypto'
|
|
9
|
-
import { LexiconDoc } from '@atproto/lexicon'
|
|
10
|
-
import { ResponseType, XrpcClient } from '@atproto/xrpc'
|
|
11
|
-
import * as xrpcServer from '../src/index.js'
|
|
12
|
-
import { logger } from '../src/logger.js'
|
|
13
|
-
import {
|
|
14
|
-
buildAddLexicons,
|
|
15
|
-
buildMethodLexicons,
|
|
16
|
-
closeServer,
|
|
17
|
-
createServer,
|
|
18
|
-
} from './_util.js'
|
|
19
|
-
|
|
20
|
-
const BLOB_LIMIT = 5000
|
|
21
|
-
|
|
22
|
-
const LEXICONS = [
|
|
23
|
-
{
|
|
24
|
-
lexicon: 1,
|
|
25
|
-
id: 'io.example.validationTest',
|
|
26
|
-
defs: {
|
|
27
|
-
main: {
|
|
28
|
-
type: 'procedure',
|
|
29
|
-
input: {
|
|
30
|
-
encoding: 'application/json',
|
|
31
|
-
schema: {
|
|
32
|
-
type: 'object',
|
|
33
|
-
required: ['foo'],
|
|
34
|
-
properties: {
|
|
35
|
-
foo: { type: 'string' },
|
|
36
|
-
bar: { type: 'integer' },
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
output: {
|
|
41
|
-
encoding: 'application/json',
|
|
42
|
-
schema: {
|
|
43
|
-
type: 'object',
|
|
44
|
-
required: ['foo'],
|
|
45
|
-
properties: {
|
|
46
|
-
foo: { type: 'string' },
|
|
47
|
-
bar: { type: 'integer' },
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
lexicon: 1,
|
|
56
|
-
id: 'io.example.validationTestTwo',
|
|
57
|
-
defs: {
|
|
58
|
-
main: {
|
|
59
|
-
type: 'query',
|
|
60
|
-
output: {
|
|
61
|
-
encoding: 'application/json',
|
|
62
|
-
schema: {
|
|
63
|
-
type: 'object',
|
|
64
|
-
required: ['foo'],
|
|
65
|
-
properties: {
|
|
66
|
-
foo: { type: 'string' },
|
|
67
|
-
bar: { type: 'integer' },
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
lexicon: 1,
|
|
76
|
-
id: 'io.example.blobTest',
|
|
77
|
-
defs: {
|
|
78
|
-
main: {
|
|
79
|
-
type: 'procedure',
|
|
80
|
-
input: {
|
|
81
|
-
encoding: '*/*',
|
|
82
|
-
},
|
|
83
|
-
output: {
|
|
84
|
-
encoding: 'application/json',
|
|
85
|
-
schema: {
|
|
86
|
-
type: 'object',
|
|
87
|
-
required: ['cid'],
|
|
88
|
-
properties: {
|
|
89
|
-
cid: { type: 'string' },
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
] as const satisfies LexiconDoc[]
|
|
97
|
-
|
|
98
|
-
const handlers = {
|
|
99
|
-
'io.example.validationTest': (ctx: xrpcServer.HandlerContext) => {
|
|
100
|
-
assert(!(ctx.input?.body instanceof Readable), 'Input is readable')
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
encoding: 'application/json',
|
|
104
|
-
body: ctx.input?.body ?? null,
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
'io.example.validationTestTwo': () => {
|
|
108
|
-
return {
|
|
109
|
-
encoding: 'application/json',
|
|
110
|
-
body: { wrong: 'data' },
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
'io.example.blobTest': async (ctx: xrpcServer.HandlerContext) => {
|
|
114
|
-
assert(ctx.input?.body != null, 'Input body is required')
|
|
115
|
-
const buffer = await consumeInput(ctx.input.body)
|
|
116
|
-
const cid = await cidForCbor(buffer)
|
|
117
|
-
return {
|
|
118
|
-
encoding: 'application/json',
|
|
119
|
-
body: { cid: cid.toString() },
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const buildServer of [buildMethodLexicons, buildAddLexicons]) {
|
|
125
|
-
describe(buildServer, () => {
|
|
126
|
-
let s: http.Server
|
|
127
|
-
let client: XrpcClient
|
|
128
|
-
let url: string
|
|
129
|
-
beforeAll(async () => {
|
|
130
|
-
const server = await buildServer(LEXICONS, handlers, {
|
|
131
|
-
payload: {
|
|
132
|
-
blobLimit: BLOB_LIMIT,
|
|
133
|
-
},
|
|
134
|
-
})
|
|
135
|
-
s = await createServer(server)
|
|
136
|
-
const { port } = s.address() as AddressInfo
|
|
137
|
-
url = `http://localhost:${port}`
|
|
138
|
-
client = new XrpcClient(url, LEXICONS)
|
|
139
|
-
})
|
|
140
|
-
afterAll(async () => {
|
|
141
|
-
if (s) await closeServer(s)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
test('io.example.validationTest', async () => {
|
|
145
|
-
const res = await client.call(
|
|
146
|
-
'io.example.validationTest',
|
|
147
|
-
{},
|
|
148
|
-
{ foo: 'hello', bar: 123 },
|
|
149
|
-
)
|
|
150
|
-
expect(res.success).toBe(true)
|
|
151
|
-
expect(res.data.foo).toBe('hello')
|
|
152
|
-
expect(res.data.bar).toBe(123)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test('requires content-type when body is expected', async () => {
|
|
156
|
-
await expect(
|
|
157
|
-
client.call('io.example.validationTest', {}),
|
|
158
|
-
).rejects.toMatchObject({
|
|
159
|
-
message: 'Request encoding (Content-Type) required but not provided',
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
test('validates required input properties', async () => {
|
|
163
|
-
await expect(
|
|
164
|
-
client.call('io.example.validationTest', {}, {}),
|
|
165
|
-
).rejects.toMatchObject({
|
|
166
|
-
error: 'InvalidRequest',
|
|
167
|
-
message: expect.stringContaining('foo'),
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
test('validates input property types', async () => {
|
|
171
|
-
await expect(
|
|
172
|
-
client.call('io.example.validationTest', {}, { foo: 123 }),
|
|
173
|
-
).rejects.toMatchObject({
|
|
174
|
-
error: 'InvalidRequest',
|
|
175
|
-
message: expect.stringContaining('foo'),
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
test('rejects invalid encoding for object data', async () => {
|
|
179
|
-
await expect(
|
|
180
|
-
client.call(
|
|
181
|
-
'io.example.validationTest',
|
|
182
|
-
{},
|
|
183
|
-
{ foo: 'hello', bar: 123 },
|
|
184
|
-
{ encoding: 'image/jpeg' },
|
|
185
|
-
),
|
|
186
|
-
).rejects.toMatchObject({
|
|
187
|
-
message: `Unable to encode object as image/jpeg data`,
|
|
188
|
-
})
|
|
189
|
-
})
|
|
190
|
-
test('rejects image/jpeg content-type for json schema', async () => {
|
|
191
|
-
await expect(
|
|
192
|
-
client.call(
|
|
193
|
-
'io.example.validationTest',
|
|
194
|
-
{},
|
|
195
|
-
// Does not need to be a valid jpeg
|
|
196
|
-
new Blob([randomBytes(123)], { type: 'image/jpeg' }),
|
|
197
|
-
),
|
|
198
|
-
).rejects.toMatchObject({
|
|
199
|
-
message: `Wrong request encoding (Content-Type): image/jpeg`,
|
|
200
|
-
})
|
|
201
|
-
})
|
|
202
|
-
test('rejects multipart/form-data content-type for json schema', async () => {
|
|
203
|
-
await expect(
|
|
204
|
-
client.call(
|
|
205
|
-
'io.example.validationTest',
|
|
206
|
-
{},
|
|
207
|
-
(() => {
|
|
208
|
-
const formData = new FormData()
|
|
209
|
-
formData.append('foo', 'bar')
|
|
210
|
-
return formData
|
|
211
|
-
})(),
|
|
212
|
-
),
|
|
213
|
-
).rejects.toMatchObject({
|
|
214
|
-
message: `Wrong request encoding (Content-Type): multipart/form-data`,
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
test('rejects application/x-www-form-urlencoded content-type for json schema', async () => {
|
|
218
|
-
await expect(
|
|
219
|
-
client.call(
|
|
220
|
-
'io.example.validationTest',
|
|
221
|
-
{},
|
|
222
|
-
new URLSearchParams([['foo', 'bar']]),
|
|
223
|
-
),
|
|
224
|
-
).rejects.toMatchObject({
|
|
225
|
-
message: `Wrong request encoding (Content-Type): application/x-www-form-urlencoded`,
|
|
226
|
-
})
|
|
227
|
-
})
|
|
228
|
-
test('rejects application/octet-stream blob for json schema', async () => {
|
|
229
|
-
await expect(
|
|
230
|
-
client.call(
|
|
231
|
-
'io.example.validationTest',
|
|
232
|
-
{},
|
|
233
|
-
new Blob([new Uint8Array([1])]),
|
|
234
|
-
),
|
|
235
|
-
).rejects.toMatchObject({
|
|
236
|
-
message: `Wrong request encoding (Content-Type): application/octet-stream`,
|
|
237
|
-
})
|
|
238
|
-
})
|
|
239
|
-
test('rejects application/octet-stream readable stream for json schema', async () => {
|
|
240
|
-
await expect(
|
|
241
|
-
client.call(
|
|
242
|
-
'io.example.validationTest',
|
|
243
|
-
{},
|
|
244
|
-
new ReadableStream({
|
|
245
|
-
pull(ctrl) {
|
|
246
|
-
ctrl.enqueue(new Uint8Array([1]))
|
|
247
|
-
ctrl.close()
|
|
248
|
-
},
|
|
249
|
-
}),
|
|
250
|
-
),
|
|
251
|
-
).rejects.toMatchObject({
|
|
252
|
-
message: `Wrong request encoding (Content-Type): application/octet-stream`,
|
|
253
|
-
})
|
|
254
|
-
})
|
|
255
|
-
test('rejects application/octet-stream uint8array for json schema', async () => {
|
|
256
|
-
await expect(
|
|
257
|
-
client.call('io.example.validationTest', {}, new Uint8Array([1])),
|
|
258
|
-
).rejects.toMatchObject({
|
|
259
|
-
message: `Wrong request encoding (Content-Type): application/octet-stream`,
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
test('validation errors on procedures include details in logs', async () => {
|
|
264
|
-
// 500 responses don't include details, so we nab details from the logger.
|
|
265
|
-
using spy = jest.spyOn(logger, 'error')
|
|
266
|
-
|
|
267
|
-
await expect(client.call('io.example.validationTestTwo')).rejects.toThrow(
|
|
268
|
-
'Internal Server Error',
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
expect(spy).toHaveBeenCalledWith(
|
|
272
|
-
expect.objectContaining({
|
|
273
|
-
err: expect.objectContaining({
|
|
274
|
-
message: expect.stringContaining('foo'),
|
|
275
|
-
}),
|
|
276
|
-
}),
|
|
277
|
-
expect.stringContaining('InternalServerError error'),
|
|
278
|
-
)
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
it('supports ArrayBuffers', async () => {
|
|
282
|
-
const bytes = randomBytes(1024)
|
|
283
|
-
const expectedCid = await cidForCbor(bytes)
|
|
284
|
-
|
|
285
|
-
const bytesResponse = await client.call(
|
|
286
|
-
'io.example.blobTest',
|
|
287
|
-
{},
|
|
288
|
-
bytes,
|
|
289
|
-
{
|
|
290
|
-
encoding: 'application/octet-stream',
|
|
291
|
-
},
|
|
292
|
-
)
|
|
293
|
-
expect(bytesResponse.data.cid).toEqual(expectedCid.toString())
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
it('supports empty payload on procedues with encoding', async () => {
|
|
297
|
-
const bytes = new Uint8Array(0)
|
|
298
|
-
const expectedCid = await cidForCbor(bytes)
|
|
299
|
-
const bytesResponse = await client.call('io.example.blobTest', {}, bytes)
|
|
300
|
-
expect(bytesResponse.data.cid).toEqual(expectedCid.toString())
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('supports upload of empty txt file', async () => {
|
|
304
|
-
const txtFile = new Blob([], { type: 'text/plain' })
|
|
305
|
-
const expectedCid = await cidForCbor(await txtFile.arrayBuffer())
|
|
306
|
-
const fileResponse = await client.call('io.example.blobTest', {}, txtFile)
|
|
307
|
-
expect(fileResponse.data.cid).toEqual(expectedCid.toString())
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
it('supports upload of json data', async () => {
|
|
311
|
-
const jsonFile = new Blob(
|
|
312
|
-
[Buffer.from(`{"foo":"bar","baz":[3, null]}`)],
|
|
313
|
-
{
|
|
314
|
-
type: 'application/json',
|
|
315
|
-
},
|
|
316
|
-
)
|
|
317
|
-
const expectedCid = await cidForCbor(await jsonFile.arrayBuffer())
|
|
318
|
-
const fileResponse = await client.call(
|
|
319
|
-
'io.example.blobTest',
|
|
320
|
-
{},
|
|
321
|
-
jsonFile,
|
|
322
|
-
)
|
|
323
|
-
expect(fileResponse.data.cid).toEqual(expectedCid.toString())
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
it('supports ArrayBufferView', async () => {
|
|
327
|
-
const bytes = randomBytes(1024)
|
|
328
|
-
const expectedCid = await cidForCbor(bytes)
|
|
329
|
-
|
|
330
|
-
const bufferResponse = await client.call(
|
|
331
|
-
'io.example.blobTest',
|
|
332
|
-
{},
|
|
333
|
-
Buffer.from(bytes),
|
|
334
|
-
)
|
|
335
|
-
expect(bufferResponse.data.cid).toEqual(expectedCid.toString())
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
it('supports Blob', async () => {
|
|
339
|
-
const bytes = randomBytes(1024)
|
|
340
|
-
const expectedCid = await cidForCbor(bytes)
|
|
341
|
-
|
|
342
|
-
const blobResponse = await client.call(
|
|
343
|
-
'io.example.blobTest',
|
|
344
|
-
{},
|
|
345
|
-
new Blob([bytes], { type: 'application/octet-stream' }),
|
|
346
|
-
)
|
|
347
|
-
expect(blobResponse.data.cid).toEqual(expectedCid.toString())
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
it('supports Blob without explicit type', async () => {
|
|
351
|
-
const bytes = randomBytes(1024)
|
|
352
|
-
const expectedCid = await cidForCbor(bytes)
|
|
353
|
-
|
|
354
|
-
const blobResponse = await client.call(
|
|
355
|
-
'io.example.blobTest',
|
|
356
|
-
{},
|
|
357
|
-
new Blob([bytes]),
|
|
358
|
-
)
|
|
359
|
-
expect(blobResponse.data.cid).toEqual(expectedCid.toString())
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('supports ReadableStream', async () => {
|
|
363
|
-
const bytes = randomBytes(1024)
|
|
364
|
-
const expectedCid = await cidForCbor(bytes)
|
|
365
|
-
|
|
366
|
-
const streamResponse = await client.call(
|
|
367
|
-
'io.example.blobTest',
|
|
368
|
-
{},
|
|
369
|
-
// ReadableStream.from not available in node < 20
|
|
370
|
-
new ReadableStream({
|
|
371
|
-
pull(ctrl) {
|
|
372
|
-
ctrl.enqueue(bytes)
|
|
373
|
-
ctrl.close()
|
|
374
|
-
},
|
|
375
|
-
}),
|
|
376
|
-
)
|
|
377
|
-
expect(streamResponse.data.cid).toEqual(expectedCid.toString())
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
it('supports blob uploads', async () => {
|
|
381
|
-
const bytes = randomBytes(1024)
|
|
382
|
-
const expectedCid = await cidForCbor(bytes)
|
|
383
|
-
|
|
384
|
-
const { data } = await client.call('io.example.blobTest', {}, bytes, {
|
|
385
|
-
encoding: 'application/octet-stream',
|
|
386
|
-
})
|
|
387
|
-
expect(data.cid).toEqual(expectedCid.toString())
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
it(`supports identity encoding`, async () => {
|
|
391
|
-
const bytes = randomBytes(1024)
|
|
392
|
-
const expectedCid = await cidForCbor(bytes)
|
|
393
|
-
|
|
394
|
-
const { data } = await client.call('io.example.blobTest', {}, bytes, {
|
|
395
|
-
encoding: 'application/octet-stream',
|
|
396
|
-
headers: { 'content-encoding': 'identity' },
|
|
397
|
-
})
|
|
398
|
-
expect(data.cid).toEqual(expectedCid.toString())
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
it('supports gzip encoding', async () => {
|
|
402
|
-
const bytes = randomBytes(1024)
|
|
403
|
-
const expectedCid = await cidForCbor(bytes)
|
|
404
|
-
|
|
405
|
-
const { data } = await client.call(
|
|
406
|
-
'io.example.blobTest',
|
|
407
|
-
{},
|
|
408
|
-
gzipSync(bytes),
|
|
409
|
-
{
|
|
410
|
-
encoding: 'application/octet-stream',
|
|
411
|
-
headers: {
|
|
412
|
-
'content-encoding': 'gzip',
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
)
|
|
416
|
-
expect(data.cid).toEqual(expectedCid.toString())
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
it('supports deflate encoding', async () => {
|
|
420
|
-
const bytes = randomBytes(1024)
|
|
421
|
-
const expectedCid = await cidForCbor(bytes)
|
|
422
|
-
|
|
423
|
-
const { data } = await client.call(
|
|
424
|
-
'io.example.blobTest',
|
|
425
|
-
{},
|
|
426
|
-
deflateSync(bytes),
|
|
427
|
-
{
|
|
428
|
-
encoding: 'application/octet-stream',
|
|
429
|
-
headers: {
|
|
430
|
-
'content-encoding': 'deflate',
|
|
431
|
-
},
|
|
432
|
-
},
|
|
433
|
-
)
|
|
434
|
-
expect(data.cid).toEqual(expectedCid.toString())
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it('supports br encoding', async () => {
|
|
438
|
-
const bytes = randomBytes(1024)
|
|
439
|
-
const expectedCid = await cidForCbor(bytes)
|
|
440
|
-
|
|
441
|
-
const { data } = await client.call(
|
|
442
|
-
'io.example.blobTest',
|
|
443
|
-
{},
|
|
444
|
-
brotliCompressSync(bytes),
|
|
445
|
-
{
|
|
446
|
-
encoding: 'application/octet-stream',
|
|
447
|
-
headers: {
|
|
448
|
-
'content-encoding': 'br',
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
)
|
|
452
|
-
expect(data.cid).toEqual(expectedCid.toString())
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
it('supports multiple encodings', async () => {
|
|
456
|
-
const bytes = randomBytes(1024)
|
|
457
|
-
const expectedCid = await cidForCbor(bytes)
|
|
458
|
-
|
|
459
|
-
const { data } = await client.call(
|
|
460
|
-
'io.example.blobTest',
|
|
461
|
-
{},
|
|
462
|
-
brotliCompressSync(deflateSync(gzipSync(bytes))),
|
|
463
|
-
{
|
|
464
|
-
encoding: 'application/octet-stream',
|
|
465
|
-
headers: {
|
|
466
|
-
'content-encoding':
|
|
467
|
-
'gzip, identity, deflate, identity, br, identity',
|
|
468
|
-
},
|
|
469
|
-
},
|
|
470
|
-
)
|
|
471
|
-
expect(data.cid).toEqual(expectedCid.toString())
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
it('fails gracefully on invalid encodings', async () => {
|
|
475
|
-
const bytes = randomBytes(1024)
|
|
476
|
-
|
|
477
|
-
const promise = client.call(
|
|
478
|
-
'io.example.blobTest',
|
|
479
|
-
{},
|
|
480
|
-
brotliCompressSync(bytes),
|
|
481
|
-
{
|
|
482
|
-
encoding: 'application/octet-stream',
|
|
483
|
-
headers: {
|
|
484
|
-
'content-encoding': 'gzip',
|
|
485
|
-
},
|
|
486
|
-
},
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
await expect(promise).rejects.toThrow('unable to read input')
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
it('supports empty payload', async () => {
|
|
493
|
-
const bytes = new Uint8Array(0)
|
|
494
|
-
const expectedCid = await cidForCbor(bytes)
|
|
495
|
-
|
|
496
|
-
// Using "undefined" as body to avoid encoding as lexicon { $bytes: "<base64>" }
|
|
497
|
-
const result = await client.call('io.example.blobTest', {}, bytes, {
|
|
498
|
-
encoding: 'text/plain',
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
expect(result.data.cid).toEqual(expectedCid.toString())
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
it('supports max blob size (based on content-length)', async () => {
|
|
505
|
-
const bytes = randomBytes(BLOB_LIMIT + 1)
|
|
506
|
-
|
|
507
|
-
// Exactly the number of allowed bytes
|
|
508
|
-
await client.call('io.example.blobTest', {}, bytes.slice(0, BLOB_LIMIT), {
|
|
509
|
-
encoding: 'application/octet-stream',
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
// Over the number of allowed bytes
|
|
513
|
-
const promise = client.call('io.example.blobTest', {}, bytes, {
|
|
514
|
-
encoding: 'application/octet-stream',
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
await expect(promise).rejects.toThrow('request entity too large')
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
it('supports max blob size (missing content-length)', async () => {
|
|
521
|
-
// We stream bytes in these tests so that content-length isn't included.
|
|
522
|
-
const bytes = randomBytes(BLOB_LIMIT + 1)
|
|
523
|
-
|
|
524
|
-
// Exactly the number of allowed bytes
|
|
525
|
-
await client.call(
|
|
526
|
-
'io.example.blobTest',
|
|
527
|
-
{},
|
|
528
|
-
bytesToReadableStream(bytes.slice(0, BLOB_LIMIT)),
|
|
529
|
-
{
|
|
530
|
-
encoding: 'application/octet-stream',
|
|
531
|
-
},
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
// Over the number of allowed bytes.
|
|
535
|
-
const promise = client.call(
|
|
536
|
-
'io.example.blobTest',
|
|
537
|
-
{},
|
|
538
|
-
bytesToReadableStream(bytes),
|
|
539
|
-
{
|
|
540
|
-
encoding: 'application/octet-stream',
|
|
541
|
-
},
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
await expect(promise).rejects.toThrow('request entity too large')
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
it('requires any parsable Content-Type for blob uploads', async () => {
|
|
548
|
-
// not a real mimetype, but correct syntax
|
|
549
|
-
await client.call('io.example.blobTest', {}, randomBytes(BLOB_LIMIT), {
|
|
550
|
-
encoding: 'some/thing',
|
|
551
|
-
})
|
|
552
|
-
})
|
|
553
|
-
|
|
554
|
-
it('errors on an empty Content-type on blob upload', async () => {
|
|
555
|
-
// empty mimetype, but correct syntax
|
|
556
|
-
const res = await fetch(`${url}/xrpc/io.example.blobTest`, {
|
|
557
|
-
method: 'post',
|
|
558
|
-
headers: { 'Content-Type': '' },
|
|
559
|
-
body: randomBytes(BLOB_LIMIT),
|
|
560
|
-
// @ts-ignore see note in @atproto/xrpc/client.ts
|
|
561
|
-
duplex: 'half',
|
|
562
|
-
})
|
|
563
|
-
const resBody = await res.json()
|
|
564
|
-
const status = res.status
|
|
565
|
-
expect(status).toBe(400)
|
|
566
|
-
expect(resBody).toMatchObject({
|
|
567
|
-
error: 'InvalidRequest',
|
|
568
|
-
message: 'Request encoding (Content-Type) required but not provided',
|
|
569
|
-
})
|
|
570
|
-
})
|
|
571
|
-
})
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const bytesToReadableStream = (bytes: Uint8Array): ReadableStream => {
|
|
575
|
-
// not using ReadableStream.from(), which lacks support in some contexts including nodejs v18.
|
|
576
|
-
return new ReadableStream({
|
|
577
|
-
pull(ctrl) {
|
|
578
|
-
ctrl.enqueue(bytes)
|
|
579
|
-
ctrl.close()
|
|
580
|
-
},
|
|
581
|
-
})
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
async function consumeInput(
|
|
585
|
-
input: Readable | string | object,
|
|
586
|
-
): Promise<Buffer> {
|
|
587
|
-
if (Buffer.isBuffer(input)) {
|
|
588
|
-
return input
|
|
589
|
-
}
|
|
590
|
-
if (typeof input === 'string') {
|
|
591
|
-
return Buffer.from(input)
|
|
592
|
-
}
|
|
593
|
-
if (input instanceof Readable) {
|
|
594
|
-
try {
|
|
595
|
-
return Buffer.concat(await input.toArray())
|
|
596
|
-
} catch (err) {
|
|
597
|
-
if (err instanceof xrpcServer.XRPCError) {
|
|
598
|
-
throw err
|
|
599
|
-
} else {
|
|
600
|
-
throw new xrpcServer.XRPCError(
|
|
601
|
-
ResponseType.InvalidRequest,
|
|
602
|
-
'unable to read input',
|
|
603
|
-
)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
throw new Error('Invalid input')
|
|
608
|
-
}
|