Skip to content

Commit f9f8f47

Browse files
committed
feat: enhance AUIApplicationBridge with binary response handling and add tests
1 parent 018dd16 commit f9f8f47

4 files changed

Lines changed: 115 additions & 36 deletions

File tree

apps/sensenet/docs/auiapplications.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,15 @@ type BridgeResponse = {
9999
}
100100
text(): Promise<string>
101101
json(): Promise<any>
102+
arrayBuffer(): Promise<ArrayBuffer>
103+
blob(): Promise<Blob>
102104
}
103105
```
104106
107+
Binary responses are transferred as `ArrayBuffer`, so downloads from endpoints such as
108+
`/binaryhandler.ashx?nodeid=...&propertyname=Binary` can be read with `blob()` or
109+
`arrayBuffer()` without converting the body through text first.
110+
105111
Requests are restricted to the current repository origin. Cross-repository and arbitrary external requests are rejected by the parent Admin UI.
106112
107113
## Admin UI Route Location

apps/sensenet/src/components/content/AUIApplicationView.tsx

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useLocation } from 'react-router-dom'
77
import {
88
AUIApplicationBridgeLocation,
99
AUIApplicationBridgeTheme,
10+
createBridgeFetchResponse,
1011
createBridgeLocation,
1112
createBridgeTheme,
1213
} from './auiapplication-bridge'
@@ -104,18 +105,39 @@ const createApplicationDocument = (
104105
return headers;
105106
};
106107
107-
const createBridgeResponse = (response) => ({
108-
ok: response.ok,
109-
status: response.status,
110-
statusText: response.statusText,
111-
url: response.url,
112-
headers: {
113-
get: (name) => response.headers[String(name).toLowerCase()] || null,
114-
entries: () => Object.entries(response.headers),
115-
},
116-
text: () => Promise.resolve(response.body),
117-
json: () => Promise.resolve(JSON.parse(response.body)),
118-
});
108+
const createBodyBuffer = (body) => {
109+
if (ArrayBuffer.isView(body)) {
110+
return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
111+
}
112+
113+
if (body && typeof body.byteLength === 'number') {
114+
return body.slice(0);
115+
}
116+
117+
return new TextEncoder().encode(String(body || '')).buffer;
118+
};
119+
120+
const createBridgeResponse = (response) => {
121+
const bodyBuffer = createBodyBuffer(response.body);
122+
const getHeader = (name) => response.headers[String(name).toLowerCase()] || null;
123+
const getBodyCopy = () => bodyBuffer.slice(0);
124+
const getBodyText = () => Promise.resolve(new TextDecoder().decode(getBodyCopy()));
125+
126+
return {
127+
ok: response.ok,
128+
status: response.status,
129+
statusText: response.statusText,
130+
url: response.url,
131+
headers: {
132+
get: getHeader,
133+
entries: () => Object.entries(response.headers),
134+
},
135+
text: getBodyText,
136+
json: () => getBodyText().then((body) => JSON.parse(body)),
137+
arrayBuffer: () => Promise.resolve(getBodyCopy()),
138+
blob: () => Promise.resolve(new Blob([getBodyCopy()], { type: getHeader('content-type') || '' })),
139+
};
140+
};
119141
120142
window.addEventListener('message', (event) => {
121143
const data = event.data || {};
@@ -293,37 +315,38 @@ export const AUIApplicationView: React.FC = () => {
293315
return
294316
}
295317

296-
const postResponse = (message: Record<string, unknown>) => {
297-
frameRef.current?.contentWindow?.postMessage(
298-
{
299-
source: AUI_APPLICATION_CONTENT_TYPE,
300-
requestId: data.requestId,
301-
...message,
302-
},
303-
'*',
304-
)
318+
const postResponse = (message: Record<string, unknown>, transfer?: Transferable[]) => {
319+
const targetWindow = frameRef.current?.contentWindow
320+
const responseMessage = {
321+
source: AUI_APPLICATION_CONTENT_TYPE,
322+
requestId: data.requestId,
323+
...message,
324+
}
325+
326+
if (!targetWindow) {
327+
return
328+
}
329+
330+
if (transfer?.length) {
331+
targetWindow.postMessage(responseMessage, '*', transfer)
332+
return
333+
}
334+
335+
targetWindow.postMessage(responseMessage, '*')
305336
}
306337

307338
try {
308339
const requestUrl = getRepositoryRequestUrl(repository.configuration.repositoryUrl, data.input)
309340
const response = await repository.fetch(requestUrl, getBridgeRequestInit(data.init))
310-
const headers: Record<string, string> = {}
341+
const bridgeResponse = await createBridgeFetchResponse(response)
311342

312-
response.headers.forEach((value, key) => {
313-
headers[key.toLowerCase()] = value
314-
})
315-
316-
postResponse({
317-
type: 'fetch:success',
318-
response: {
319-
ok: response.ok,
320-
status: response.status,
321-
statusText: response.statusText,
322-
url: response.url,
323-
headers,
324-
body: await response.text(),
343+
postResponse(
344+
{
345+
type: 'fetch:success',
346+
response: bridgeResponse,
325347
},
326-
})
348+
[bridgeResponse.body],
349+
)
327350
} catch (error) {
328351
postResponse({
329352
type: 'fetch:error',

apps/sensenet/src/components/content/auiapplication-bridge.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,32 @@ export type AUIApplicationBridgeLocation = {
88

99
export type AUIApplicationBridgeTheme = 'light' | 'dark'
1010

11+
export type AUIApplicationBridgeFetchResponse = {
12+
ok: boolean
13+
status: number
14+
statusText: string
15+
url: string
16+
headers: Record<string, string>
17+
body: ArrayBuffer
18+
}
19+
20+
export const createBridgeFetchResponse = async (response: Response): Promise<AUIApplicationBridgeFetchResponse> => {
21+
const headers: Record<string, string> = {}
22+
23+
response.headers.forEach((value, key) => {
24+
headers[key.toLowerCase()] = value
25+
})
26+
27+
return {
28+
ok: response.ok,
29+
status: response.status,
30+
statusText: response.statusText,
31+
url: response.url,
32+
headers,
33+
body: await response.arrayBuffer(),
34+
}
35+
}
36+
1137
export const createBridgeLocation = (
1238
adminUiUrl: string,
1339
location: Pick<Location, 'pathname' | 'search' | 'hash'>,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createBridgeFetchResponse } from '../src/components/content/auiapplication-bridge'
2+
3+
describe('createBridgeFetchResponse', () => {
4+
it('serializes binary responses as ArrayBuffer without corrupting bytes', async () => {
5+
const bytes = new Uint8Array([0x00, 0xff, 0x80, 0x41, 0x0a])
6+
const response = new Response(bytes, {
7+
status: 200,
8+
statusText: 'OK',
9+
headers: {
10+
'Content-Type': 'application/octet-stream',
11+
},
12+
})
13+
14+
const bridgeResponse = await createBridgeFetchResponse(response)
15+
16+
expect(bridgeResponse.ok).toBe(true)
17+
expect(bridgeResponse.status).toBe(200)
18+
expect(bridgeResponse.headers['content-type']).toBe('application/octet-stream')
19+
expect(Array.from(new Uint8Array(bridgeResponse.body))).toEqual(Array.from(bytes))
20+
21+
const blob = new Blob([bridgeResponse.body], { type: bridgeResponse.headers['content-type'] })
22+
expect(Array.from(new Uint8Array(await blob.arrayBuffer()))).toEqual(Array.from(bytes))
23+
})
24+
})

0 commit comments

Comments
 (0)