Skip to content

Commit 0c21217

Browse files
committed
fix: arraybuffer
1 parent fdde836 commit 0c21217

File tree

2 files changed

+58
-37
lines changed

2 files changed

+58
-37
lines changed

src/services/api/viewer.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -417,26 +417,24 @@ export class ViewerAPI extends BaseYdbAPI {
417417
requestConfig: {
418418
signal,
419419
'axios-retry': {retries: 0}, // No retries for streaming
420-
responseType: 'text',
420+
responseType: 'arraybuffer',
421421
},
422422
headers: {
423423
...(params.tracingLevel ? {'X-Trace-Verbosity': params.tracingLevel} : {}),
424424
Accept: 'multipart/x-mixed-replace',
425425
},
426426
onDownloadProgress: (progressEvent) => {
427-
const response = progressEvent.event.target as XMLHttpRequest;
427+
const xhr = progressEvent.event.target as XMLHttpRequest;
428428
const {chunks, lastProcessedLength: currentLength} = parseMultipart({
429-
responseText: response.responseText,
429+
response: xhr.response,
430430
lastProcessedLength,
431431
});
432432
lastProcessedLength = currentLength;
433433
const mergedChunks = new Map<number, StreamDataChunk>();
434434

435435
for (const chunk of chunks) {
436436
if (isSessionChunk(chunk)) {
437-
const traceId = response
438-
.getResponseHeader('traceresponse')
439-
?.split('-')[1];
437+
const traceId = xhr.getResponseHeader('traceresponse')?.split('-')[1];
440438

441439
chunk.meta.trace_id = traceId;
442440
onChunk(chunk);

src/services/parsers/parseMultipart.ts

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,74 +5,103 @@ interface MultipartResult {
55
lastProcessedLength: number;
66
}
77

8-
const CRLF = '\r\n';
9-
const HEADER_VALUE_DELIMITER = ': ';
8+
const CRLF_BYTES = new Uint8Array([13, 10]); // \r\n
9+
const HEADER_VALUE_DELIMITER_BYTES = new TextEncoder().encode(': ');
10+
11+
function arrayIndexOf(haystack: Uint8Array, needle: Uint8Array, start = 0): number {
12+
for (let i = start; i <= haystack.length - needle.length; i++) {
13+
let found = true;
14+
for (let j = 0; j < needle.length; j++) {
15+
if (haystack[i + j] !== needle[j]) {
16+
found = false;
17+
break;
18+
}
19+
}
20+
if (found) {
21+
return i;
22+
}
23+
}
24+
return -1;
25+
}
1026

11-
// temporary spike for backend problems
12-
const BACKEND_LAST_CHUNK_BOUNDARY = '--boundary--';
27+
function arrayStartsWith(array: Uint8Array, prefix: Uint8Array, start = 0): boolean {
28+
if (start + prefix.length > array.length) {
29+
return false;
30+
}
31+
for (let i = 0; i < prefix.length; i++) {
32+
if (array[start + i] !== prefix[i]) {
33+
return false;
34+
}
35+
}
36+
return true;
37+
}
1338

14-
function getContentHeadersData(data: string, startPos: number): [number, number] {
39+
function getContentHeadersData(data: Uint8Array, startPos: number): [number, number] {
1540
let contentLength = 0;
1641
let pos = startPos;
42+
const decoder = new TextDecoder();
1743

1844
while (pos < data.length) {
1945
// Check for end of headers
20-
if (data.startsWith(CRLF, pos)) {
21-
return [contentLength, pos + CRLF.length];
46+
if (arrayStartsWith(data, CRLF_BYTES, pos)) {
47+
return [contentLength, pos + CRLF_BYTES.length];
2248
}
2349

24-
const nextCRLF = data.indexOf(CRLF, pos);
50+
const nextCRLF = arrayIndexOf(data, CRLF_BYTES, pos);
2551
if (nextCRLF === -1) {
2652
// Headers are incomplete
2753
return [contentLength, startPos];
2854
}
2955

3056
const line = data.slice(pos, nextCRLF);
31-
const colonIndex = line.indexOf(HEADER_VALUE_DELIMITER);
57+
const colonIndex = arrayIndexOf(line, HEADER_VALUE_DELIMITER_BYTES);
3258

3359
if (colonIndex !== -1) {
34-
const header = line.slice(0, colonIndex).toLowerCase();
35-
const value = line.slice(colonIndex + HEADER_VALUE_DELIMITER.length, nextCRLF);
60+
const header = decoder.decode(line.slice(0, colonIndex)).toLowerCase();
61+
const value = decoder.decode(
62+
line.slice(colonIndex + HEADER_VALUE_DELIMITER_BYTES.length),
63+
);
3664

37-
if (header.toLowerCase() === 'content-length') {
65+
if (header === 'content-length') {
3866
const length = parseInt(value, 10);
3967
if (!isNaN(length)) {
4068
contentLength = length;
4169
}
4270
}
4371
}
4472

45-
pos = nextCRLF + CRLF.length;
73+
pos = nextCRLF + CRLF_BYTES.length;
4674
}
4775

4876
// Headers are incomplete
4977
return [contentLength, startPos];
5078
}
5179

5280
export function parseMultipart({
53-
responseText,
81+
response,
5482
lastProcessedLength,
5583
boundary = 'boundary',
5684
}: {
57-
responseText: string;
85+
response: ArrayBuffer;
5886
lastProcessedLength: number;
5987
boundary?: string;
6088
}): MultipartResult {
61-
const data = responseText;
62-
const boundaryStr = `--${boundary}${CRLF}`;
89+
const data = new Uint8Array(response);
90+
const boundaryBytes = new TextEncoder().encode(`--${boundary}\r\n`);
6391
let pos = lastProcessedLength;
6492
let lastProcessedPos = pos;
6593
const chunks: StreamingChunk[] = [];
94+
const decoder = new TextDecoder();
6695

6796
while (pos < data.length) {
6897
// Look for boundary
69-
const boundaryPos = data.indexOf(boundaryStr, pos);
98+
const boundaryPos = arrayIndexOf(data, boundaryBytes, pos);
7099
if (boundaryPos === -1) {
71100
break;
72101
}
73102

74103
// Move position past boundary
75-
pos = boundaryPos + boundaryStr.length;
104+
pos = boundaryPos + boundaryBytes.length;
76105

77106
// Parse headers
78107
const [contentLength, contentStart] = getContentHeadersData(data, pos);
@@ -83,21 +112,15 @@ export function parseMultipart({
83112
}
84113

85114
// Check if we have enough data for the content
86-
let contentEnd = contentStart + contentLength;
115+
const contentEnd = contentStart + contentLength;
87116
if (contentEnd > data.length) {
88-
// last chunk has error with --boundary-- in content length
89-
// eslint-disable-next-line no-negated-condition
90-
if (data.indexOf(BACKEND_LAST_CHUNK_BOUNDARY, contentStart) !== -1) {
91-
contentEnd = data.indexOf(BACKEND_LAST_CHUNK_BOUNDARY, contentStart) - 2;
92-
} else {
93-
// Content is incomplete
94-
pos = lastProcessedLength;
95-
break;
96-
}
117+
// Content is incomplete
118+
pos = lastProcessedLength;
119+
break;
97120
}
98121

99-
// Extract content
100-
const content = data.slice(contentStart, contentEnd);
122+
// Extract content and decode to string
123+
const content = decoder.decode(data.slice(contentStart, contentEnd));
101124

102125
// Try to parse JSON content
103126
try {

0 commit comments

Comments
 (0)