Skip to content

Commit d03ea65

Browse files
committed
[scramjet/demo] properly tee requests in request viewer
1 parent 44da98b commit d03ea65

File tree

2 files changed

+205
-7
lines changed

2 files changed

+205
-7
lines changed

packages/scramjet/packages/demo/src/App.tsx

Lines changed: 162 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,69 @@ const getBodyPreview = (body: unknown): { preview: string; size?: number } => {
4848
return { preview: String(body) };
4949
};
5050

51+
const readStreamBody = async (stream: ReadableStream): Promise<string> => {
52+
try {
53+
return await new Response(stream).text();
54+
} catch {
55+
return "";
56+
}
57+
};
58+
59+
const readStreamBlob = async (stream: ReadableStream): Promise<Blob | null> => {
60+
try {
61+
return await new Response(stream).blob();
62+
} catch {
63+
return null;
64+
}
65+
};
66+
67+
const ensureScramjetResponse = (response: any) => {
68+
if (!response) return response;
69+
const BareResponseCtor = (globalThis as any).$scramjet?.BareResponse;
70+
if (response instanceof Response && BareResponseCtor?.fromNativeResponse) {
71+
return BareResponseCtor.fromNativeResponse(response);
72+
}
73+
if (!Array.isArray(response.rawHeaders) && response.headers) {
74+
try {
75+
response.rawHeaders = [...response.headers.entries()];
76+
} catch {
77+
response.rawHeaders = [];
78+
}
79+
}
80+
return response;
81+
};
82+
83+
const getHeaderValue = (
84+
headers: Array<[string, string]> | undefined,
85+
name: string
86+
) => {
87+
if (!headers) return undefined;
88+
const match = headers.find(
89+
([key]) => key.toLowerCase() === name.toLowerCase()
90+
);
91+
return match ? match[1] : undefined;
92+
};
93+
94+
const isMediaContentType = (contentType?: string | null) =>
95+
!!contentType &&
96+
(contentType.toLowerCase().startsWith("image/") ||
97+
contentType.toLowerCase().startsWith("video/"));
98+
99+
const getMediaUrlFromBody = (
100+
body: unknown,
101+
contentType?: string | null
102+
): { url?: string; size?: number } => {
103+
if (!isMediaContentType(contentType) || body == null) return {};
104+
if (body instanceof Blob) {
105+
return { url: URL.createObjectURL(body), size: body.size };
106+
}
107+
if (body instanceof ArrayBuffer) {
108+
const blob = new Blob([body], { type: contentType ?? "" });
109+
return { url: URL.createObjectURL(blob), size: blob.size };
110+
}
111+
return {};
112+
};
113+
51114
export const App: Component<
52115
{},
53116
{},
@@ -58,6 +121,7 @@ export const App: Component<
58121
requestSeq: number;
59122
requestIdByRequest: WeakMap<object, string>;
60123
requestStartByRequest: WeakMap<object, number>;
124+
preResponseBodyStreamByRequest: WeakMap<object, ReadableStream>;
61125
requests: RequestEntry[];
62126
selectedId: string | null;
63127
}
@@ -73,6 +137,7 @@ export const App: Component<
73137
this.requestSeq = 0;
74138
this.requestIdByRequest = new WeakMap();
75139
this.requestStartByRequest = new WeakMap();
140+
this.preResponseBodyStreamByRequest = new WeakMap();
76141

77142
this.frame = controller.createFrame(this.frameel);
78143

@@ -112,11 +177,60 @@ export const App: Component<
112177

113178
plugin.tap(
114179
this.frame.hooks.fetch.preresponse,
115-
(context: any, props: any) => {
180+
async (context: any, props: any) => {
116181
const id = this.requestIdByRequest.get(context.request as object);
117182
if (!id) return;
118-
const preHeaders = normalizeHeaders(props.response?.rawHeaders);
119-
const preBodyInfo = getBodyPreview(props.response?.body);
183+
props.response = ensureScramjetResponse(props.response);
184+
const preHeaders = normalizeHeaders(
185+
(props.response as any)?.rawHeaders
186+
);
187+
const preContentType = getHeaderValue(preHeaders, "content-type");
188+
189+
let preBodyInfo: { preview: string; size?: number } = {
190+
preview: "",
191+
};
192+
let preMediaUrl: string | undefined;
193+
if (
194+
props.response?.body &&
195+
typeof ReadableStream !== "undefined" &&
196+
props.response.body instanceof ReadableStream
197+
) {
198+
const [streamForResponse, streamForPreview] =
199+
props.response.body.tee();
200+
const [streamForStore, streamForRead] = streamForPreview.tee();
201+
if (props.response instanceof Response) {
202+
const rebuilt = new Response(streamForResponse, props.response);
203+
props.response = ensureScramjetResponse(rebuilt);
204+
} else {
205+
props.response.body = streamForResponse;
206+
}
207+
this.preResponseBodyStreamByRequest.set(
208+
context.request as object,
209+
streamForStore
210+
);
211+
if (isMediaContentType(preContentType)) {
212+
const blob = await readStreamBlob(streamForRead);
213+
if (blob) {
214+
preMediaUrl = URL.createObjectURL(blob);
215+
preBodyInfo = {
216+
preview: "",
217+
size: blob.size,
218+
};
219+
}
220+
} else {
221+
const previewText = await readStreamBody(streamForRead);
222+
preBodyInfo = getBodyPreview(previewText);
223+
}
224+
} else {
225+
const media = getMediaUrlFromBody(
226+
props.response?.body,
227+
preContentType
228+
);
229+
preMediaUrl = media.url;
230+
preBodyInfo = media.url
231+
? { preview: "", size: media.size }
232+
: getBodyPreview(props.response?.body);
233+
}
120234

121235
this.requests = this.requests.map((entry) =>
122236
entry.id === id
@@ -125,6 +239,7 @@ export const App: Component<
125239
responseHeadersPre: preHeaders,
126240
responseBodyPreviewPre: preBodyInfo.preview,
127241
responseBodySizePre: preBodyInfo.size,
242+
responseBodyMediaUrlPre: preMediaUrl,
128243
}
129244
: entry
130245
);
@@ -133,9 +248,10 @@ export const App: Component<
133248

134249
plugin.tap(
135250
this.frame.hooks.fetch.response,
136-
(context: any, props: any) => {
251+
async (context: any, props: any) => {
137252
const id = this.requestIdByRequest.get(context.request as object);
138253
if (!id) return;
254+
props.response = ensureScramjetResponse(props.response);
139255
const start = this.requestStartByRequest.get(
140256
context.request as object
141257
);
@@ -145,9 +261,48 @@ export const App: Component<
145261
: undefined;
146262
const contentType = props.response.headers?.get?.("content-type");
147263
const respHeaders = normalizeHeaders(
148-
props.response.headers?.toRawHeaders?.()
264+
props.response.headers?.toRawHeaders?.() ??
265+
props.response.headers ??
266+
(props.response as any)?.rawHeaders
149267
);
150-
const respBodyInfo = getBodyPreview(props.response.body);
268+
269+
let respBodyInfo: { preview: string; size?: number } = {
270+
preview: "",
271+
};
272+
let respMediaUrl: string | undefined;
273+
if (
274+
props.response?.body &&
275+
typeof ReadableStream !== "undefined" &&
276+
props.response.body instanceof ReadableStream
277+
) {
278+
const [streamForResponse, streamForPreview] =
279+
props.response.body.tee();
280+
if (props.response instanceof Response) {
281+
const rebuilt = new Response(streamForResponse, props.response);
282+
props.response = ensureScramjetResponse(rebuilt);
283+
} else {
284+
props.response.body = streamForResponse;
285+
}
286+
if (isMediaContentType(contentType)) {
287+
const blob = await readStreamBlob(streamForPreview);
288+
if (blob) {
289+
respMediaUrl = URL.createObjectURL(blob);
290+
respBodyInfo = { preview: "", size: blob.size };
291+
}
292+
} else {
293+
const previewText = await readStreamBody(streamForPreview);
294+
respBodyInfo = getBodyPreview(previewText);
295+
}
296+
} else {
297+
const media = getMediaUrlFromBody(
298+
props.response?.body,
299+
contentType
300+
);
301+
respMediaUrl = media.url;
302+
respBodyInfo = media.url
303+
? { preview: "", size: media.size }
304+
: getBodyPreview(props.response.body);
305+
}
151306

152307
this.requests = this.requests.map((entry) =>
153308
entry.id === id
@@ -160,6 +315,7 @@ export const App: Component<
160315
responseHeaders: respHeaders,
161316
responseBodyPreview: respBodyInfo.preview,
162317
responseBodySize: respBodyInfo.size,
318+
responseBodyMediaUrl: respMediaUrl,
163319
}
164320
: entry
165321
);

packages/scramjet/packages/demo/src/RequestViewer.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export type RequestEntry = {
2020
requestBodyPreview?: string;
2121
responseBodyPreviewPre?: string;
2222
responseBodyPreview?: string;
23+
responseBodyMediaUrlPre?: string;
24+
responseBodyMediaUrl?: string;
2325
requestBodySize?: number;
2426
responseBodySizePre?: number;
2527
responseBodySize?: number;
@@ -36,7 +38,6 @@ const languageFromContentType = (contentType?: string | null) => {
3638
if (normalized.includes("text/plain")) return "plaintext";
3739
return "plaintext";
3840
};
39-
4041
const getHeaderValue = (
4142
headers: Array<[string, string]> | undefined,
4243
name: string
@@ -48,6 +49,11 @@ const getHeaderValue = (
4849
return match ? match[1] : undefined;
4950
};
5051

52+
const isMediaContentType = (contentType?: string | null) =>
53+
!!contentType &&
54+
(contentType.toLowerCase().startsWith("image/") ||
55+
contentType.toLowerCase().startsWith("video/"));
56+
5157
export const RequestViewer: Component<
5258
{
5359
requests: RequestEntry[];
@@ -338,6 +344,34 @@ export const RequestViewer: Component<
338344
view === "pre"
339345
? selected.responseBodyPreviewPre
340346
: selected.responseBodyPreview;
347+
const mediaUrl =
348+
view === "pre"
349+
? selected.responseBodyMediaUrlPre
350+
: selected.responseBodyMediaUrl;
351+
const isMedia = isMediaContentType(selected.contentType);
352+
if (isMedia && mediaUrl) {
353+
if (
354+
selected.contentType
355+
?.toLowerCase()
356+
.startsWith("image/")
357+
) {
358+
return (
359+
<img
360+
src={mediaUrl}
361+
alt="Response preview"
362+
class="body-media"
363+
/>
364+
);
365+
}
366+
return (
367+
<video src={mediaUrl} class="body-media" controls />
368+
);
369+
}
370+
if (isMedia && !mediaUrl) {
371+
return (
372+
<div class="body-empty">(media not captured)</div>
373+
);
374+
}
341375
return body ? (
342376
<MonacoComponent
343377
value={body}
@@ -566,6 +600,14 @@ RequestViewer.style = css`
566600
font-style: italic;
567601
padding: 0.4em 0.2em;
568602
}
603+
.body-media {
604+
max-width: 100%;
605+
max-height: 480px;
606+
border-radius: 8px;
607+
border: 1px solid #222;
608+
background: #0b0b0b;
609+
display: block;
610+
}
569611
.headers-table {
570612
display: flex;
571613
flex-direction: column;

0 commit comments

Comments
 (0)