Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d2e617a
feat : expose response headers in DocsService
hyunw9 Apr 5, 2025
ed925b2
fix : restore removed comments
hyunw9 Apr 5, 2025
0c5f150
fix: Define Header type for better clarity
hyunw9 Apr 9, 2025
d788723
feat : define responseData
hyunw9 Apr 13, 2025
23fac82
feat : modify response
hyunw9 Apr 13, 2025
9b9921c
feat : modify responseData
hyunw9 Apr 13, 2025
718f68e
feat : remove unused method : extractHeaders()
hyunw9 Apr 14, 2025
9705ab2
feat : edit response Form
hyunw9 Apr 14, 2025
296645f
Merge pull request #2 from hyunw9/feat-define-ResponseData
hyunw9 Apr 14, 2025
08f344b
feat : Update unnecessary useEffect
hyunw9 Apr 16, 2025
56e3d2c
feat : Deduplicate render logic
hyunw9 Apr 16, 2025
46af150
faet : Revert header variable name
hyunw9 Apr 16, 2025
8306d0c
feat : Delete fallback parameter
hyunw9 Apr 16, 2025
62839b8
feat : Deduplicate header build logic
hyunw9 Apr 16, 2025
add4c40
Merge pull request #4 from hyunw9/feat/update-review
hyunw9 Apr 16, 2025
5e1fa52
feat : Declare list available header types
hyunw9 Apr 19, 2025
690f573
feat : Change headers type from Map to Array
hyunw9 Apr 19, 2025
61f864c
feat : Delete parsing logic
hyunw9 Apr 19, 2025
abdad9b
feat : Redesign header extracting logic
hyunw9 Apr 19, 2025
a9b8f6e
feat : Change method name
hyunw9 Apr 19, 2025
5836119
feat : Delete json parse logic
hyunw9 Apr 19, 2025
a9bab84
feat : Update header type
hyunw9 Apr 19, 2025
e0cef46
Merge pull request #6 from hyunw9/feat/update-review
hyunw9 Apr 19, 2025
aa14f38
feat : Expand ResponseData
hyunw9 Apr 29, 2025
a7dbee2
feat : Add Status, ExecutionTime, ResponseSize, TimeStamp
hyunw9 Apr 29, 2025
8f2f600
feat : Apply additional Component to DebugPage
hyunw9 Apr 29, 2025
5260cee
feat : Update Cache, Error handling logic
hyunw9 Apr 29, 2025
295debf
feat : Deduplicate preparing `ResponseData`
hyunw9 Apr 29, 2025
b4309dc
feat : Deduplicate responseData status bar logic
hyunw9 May 12, 2025
67bf1fd
feat : Update docs-client/src/lib/json-util.ts
hyunw9 May 13, 2025
7f8a887
feat : Delete static header set
hyunw9 May 13, 2025
d1ac966
Merge branch 'add-Response-Header' of https://github.com/hyunw9/armer…
hyunw9 May 13, 2025
52f2271
Merge branch 'main' into add-Response-Header
hyunw9 May 29, 2025
355f172
Merge branch 'main' into add-Response-Header
hyunw9 Jun 15, 2025
d0d668e
Merge branch 'main' into add-Response-Header
hyunw9 Jun 17, 2025
0f1501c
chore : Remove unused import pharase
hyunw9 Jun 17, 2025
25614a5
Merge branch 'add-Response-Header' of https://github.com/hyunw9/armer…
hyunw9 Jun 17, 2025
8763d92
fix : Adjust header info layout
hyunw9 Jun 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 124 additions & 22 deletions docs-client/src/containers/MethodPage/DebugPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ const toggle = (prev: boolean, override: unknown) => {

const escapeSingleQuote = (text: string) => text.replace(/'/g, "'\\''");

type Header = [name: string, value: string];

interface ResponseData {
headers: Header[];
body: string;
}

const DebugPage: React.FunctionComponent<Props> = ({
exactPathMapping,
exampleHeaders,
Expand All @@ -149,6 +156,9 @@ const DebugPage: React.FunctionComponent<Props> = ({
const [requestBody, setRequestBody] = useState('');
const [debugResponse, setDebugResponse] = useState('');
const [additionalQueries, setAdditionalQueries] = useState('');
const [debugResponseHeaders, setDebugResponseHeaders] = useState<Header[]>(
[],
);
const [additionalPath, setAdditionalPath] = useState('');
const [additionalHeaders, setAdditionalHeaders] = useState('');
const [stickyHeaders, toggleStickyHeaders] = useReducer(toggle, false);
Expand All @@ -159,13 +169,34 @@ const DebugPage: React.FunctionComponent<Props> = ({
false,
);

const [currentApiId, setCurrentApiId] = useState<string>(
method.id || method.name,
);
const [responseCache, setResponseCache] = useState<
Record<string, ResponseData>
>({});

const classes = useStyles();

const transport = TRANSPORTS.getDebugTransport(method);
if (!transport) {
throw new Error("This method doesn't have a debug transport.");
}

useEffect(() => {
Comment thread
hyunw9 marked this conversation as resolved.
const apiId = method.id || method.name;
if (apiId !== currentApiId) {
setCurrentApiId(apiId);
if (responseCache[apiId]) {
setDebugResponse(responseCache[apiId].body);
setDebugResponseHeaders(responseCache[apiId].headers);
} else {
setDebugResponse('');
setDebugResponseHeaders([]);
}
}
}, [method, currentApiId, responseCache]);

useEffect(() => {
const urlParams = new URLSearchParams(location.search);

Expand Down Expand Up @@ -202,6 +233,7 @@ const DebugPage: React.FunctionComponent<Props> = ({

if (!keepDebugResponse) {
setDebugResponse('');
setDebugResponseHeaders([]);
toggleKeepDebugResponse(false);
}
setSnackbarOpen(false);
Expand Down Expand Up @@ -307,23 +339,23 @@ const DebugPage: React.FunctionComponent<Props> = ({
escapeSingleQuote(requestBody),
);

const headers = new Headers();
headers.set('content-type', transport.getDebugMimeType());
const headersObj = new Headers();
Comment thread
hyunw9 marked this conversation as resolved.
Outdated
headersObj.set('content-type', transport.getDebugMimeType());
if (process.env.WEBPACK_DEV === 'true') {
headers.set(docServiceDebug, 'true');
headersObj.set(docServiceDebug, 'true');
}
if (serviceType === ServiceType.GRAPHQL) {
headers.set('accept', 'application/json');
headersObj.set('accept', 'application/json');
}
if (additionalHeaders) {
const entries = Object.entries(JSON.parse(additionalHeaders));
entries.forEach(([key, value]) => {
headers.set(key, String(value));
headersObj.set(key, String(value));
});
}

const headerOptions: string[] = [];
headers.forEach((value, key) => {
headersObj.forEach((value, key) => {
headerOptions.push(`-H '${key}: ${value}'`);
});

Expand Down Expand Up @@ -364,6 +396,7 @@ const DebugPage: React.FunctionComponent<Props> = ({

const onClear = useCallback(() => {
setDebugResponse('');
setDebugResponseHeaders([]);
}, []);

const executeRequest = useCallback(
Expand All @@ -390,24 +423,26 @@ const DebugPage: React.FunctionComponent<Props> = ({
const headersText = params.get('headers');
const headers = headersText ? JSON.parse(headersText) : {};

let executedDebugResponse;
try {
executedDebugResponse = await transport.send(
const { body, headers: responseHeaders } = await transport.send(
method,
headers,
parseServerRootPath(docServiceRoute),
executedRequestBody,
executedEndpointPath,
queries,
);
setDebugResponse(body);
setDebugResponseHeaders(Object.entries(responseHeaders));
setResponseCache((prev) => ({
...prev,
[currentApiId]: { body, headers: Object.entries(responseHeaders) },
}));
} catch (e) {
if (e instanceof Object) {
executedDebugResponse = e.toString();
} else {
executedDebugResponse = '<unknown>';
}
const message = e instanceof Object ? e.toString() : '<unknown>';
setDebugResponse(message);
setDebugResponseHeaders([]);
}
setDebugResponse(executedDebugResponse);
},
[
useRequestBody,
Expand All @@ -416,6 +451,7 @@ const DebugPage: React.FunctionComponent<Props> = ({
method,
transport,
docServiceRoute,
currentApiId,
],
);

Expand Down Expand Up @@ -505,6 +541,20 @@ const DebugPage: React.FunctionComponent<Props> = ({
transport,
]);

useEffect(() => {
const newApiId = method.id || method.name;
Comment thread
hyunw9 marked this conversation as resolved.
Outdated
if (newApiId !== currentApiId) {
setCurrentApiId(newApiId);
if (responseCache[newApiId]) {
setDebugResponse(responseCache[newApiId].body);
setDebugResponseHeaders(responseCache[newApiId].headers);
} else {
setDebugResponse('');
setDebugResponseHeaders([]);
}
}
}, [method, currentApiId, responseCache]);

const supportedExamplePaths = useMemo(() => {
if (
serviceType === ServiceType.HTTP ||
Expand Down Expand Up @@ -572,7 +622,7 @@ const DebugPage: React.FunctionComponent<Props> = ({
<Grid item xs={12} sm={6}>
<Grid container spacing={1}>
<Grid item xs="auto">
<Tooltip title="Copy response">
<Tooltip title="Copy response body">
<div>
<IconButton
onClick={onCopy}
Expand All @@ -596,13 +646,41 @@ const DebugPage: React.FunctionComponent<Props> = ({
</Tooltip>
</Grid>
</Grid>
<SyntaxHighlighter
language="json"
style={githubGist}
wrapLines={false}
>
{debugResponse}
</SyntaxHighlighter>
{debugResponse && (
<>
{Object.keys(debugResponseHeaders).length > 0 && (
<>
<Typography
variant="subtitle1"
style={{ marginTop: '1rem' }}
>
Response Headers:
</Typography>
<SyntaxHighlighter
language="json"
style={githubGist}
wrapLines={false}
>
{JSON.stringify(
Object.fromEntries(debugResponseHeaders),
null,
2,
)}
</SyntaxHighlighter>
</>
)}
<Typography variant="subtitle1" style={{ marginTop: '1rem' }}>
Response Body:
</Typography>
<SyntaxHighlighter
language="json"
style={githubGist}
wrapLines={false}
>
{debugResponse}
</SyntaxHighlighter>
</>
)}
</Grid>
</Grid>
<Snackbar
Expand Down Expand Up @@ -684,6 +762,30 @@ const DebugPage: React.FunctionComponent<Props> = ({
</Tooltip>
</Grid>
</Grid>
{Object.keys(debugResponseHeaders).length > 0 && (
<>
<Typography
variant="subtitle1"
style={{ marginTop: '1rem' }}
>
Response Headers:
</Typography>
<SyntaxHighlighter
language="json"
style={githubGist}
wrapLines={false}
>
{JSON.stringify(
Object.fromEntries(debugResponseHeaders),
null,
2,
)}
</SyntaxHighlighter>
</>
)}
<Typography variant="subtitle1" style={{ marginTop: '1rem' }}>
Response Body:
</Typography>
<SyntaxHighlighter
language="json"
style={githubGist}
Expand Down
30 changes: 25 additions & 5 deletions docs-client/src/lib/transports/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default abstract class Transport {
bodyJson?: string,
endpointPath?: string,
queries?: string,
): Promise<string> {
): Promise<{ body: string; headers: Record<string, string> }> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return ResponseData to preserve the header ordering and support multi-value headers. By doing so, we can remove extractHeaders below as well.

@hyunw9 hyunw9 Apr 14, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your review ! Belows are the changes :

  1. Introduced ResponseData to handle response (body + headers).
  2. Implemented multi-value headers handling using Map<string, string[]>.
  3. Updated header display logic in DebugPage to follow RFC9110 §5.2 guidelines.

RFC9110 specifies:

"A sender MUST NOT generate multiple header fields with the same field name unless the entire field value is comma-separated or the field explicitly allows multiple field lines."

Thus, I defined ResponseData in types.tsx as:

https://github.com/hyunw9/armeria/blob/9705ab2a375c66f0f383edfd526249c5f7f6c150/docs-client/src/lib/types.ts#L28-L31

Replaced Response with <ResponseData> in abstract doSend<Response>, and aggregated response headers:

https://github.com/hyunw9/armeria/blob/9705ab2a375c66f0f383edfd526249c5f7f6c150/docs-client/src/lib/transports/annotated-http.ts#L126-L133

Adjusted DebugPage header display:

https://github.com/hyunw9/armeria/blob/9705ab2a375c66f0f383edfd526249c5f7f6c150/docs-client/src/containers/MethodPage/DebugPage.tsx#L664-L672

When server set headers like this :

final HttpHeaders headers = HttpHeaders.builder()
                                         .add("x-role", "admin")
                                         .add("x-role", "editor")
                                         .add("x-role", "user")
                                         .build();

Now, multi-value headers appear correctly as below:

Response Headers

If you have any feedback or suggestions, I’d really appreciate your comments!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! It looks much better. However, I'd like you to consider the following:

  • The RFC you mentioned doesn't prohibit specifying multiple header values with the same header name if the multiple field lines are allowed explicitly.
  • At the protocol level, these two are different:
    x-role: admin, editor, user
    
    vs.
    x-role: admin
    x-role: editor
    x-role: user
    

Therefore, what do you think about using plaintext rather than JSON to render the response headers?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also note that the following are considered different, although they are semantically same:

x-role: admin
other-header: other value
x-role: user

vs.

x-role: admin
x-role: user
other-header: other value

.. which means we need to preserve the ordering, which cannot be achieved by using a Map.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, I didn’t consider the preservation of header order or merging rules.
However, after reviewing RFC 9110, I found that only headers defined using the #element syntax are safe to merge using commas.
For other headers, merging can result in loss of semantics or parsing errors.

For example, the Date header value like "Sat, 19 Apr 2025 09:00:00 GMT" includes a comma as part of the value.
Blindly splitting on commas would incorrectly produce lines like:

date: Sat
date: 19 Apr 2025 09:00:00 GMT

Initially, I implemented naive comma-splitting across all headers.
After identifying this issue, I refined the logic to selectively split only list-type headers, while preserving others unchanged.

The updated logic iterates through the response Headers object:

  1. If a header is recognized as a list-type (e.g., Accept, Cache-Control), it splits the value by comma and renders each entry individually.

  2. Otherwise, it preserves the header as a single line to avoid unintended splitting.

Example

const providedHeaders = await Promise.all(
providers.map((provider) => provider()),
);
Expand Down Expand Up @@ -59,25 +59,45 @@ export default abstract class Transport {
endpointPath,
queries,
);
const responseHeaders = this.extractHeaders(httpResponse.headers);
const responseText = await httpResponse.text();
const applicationType = httpResponse.headers.get('content-type') || '';
if (applicationType.indexOf('json') >= 0) {
try {
const json = JSONbig.parse(responseText);
const prettified = jsonPrettify(JSONbig.stringify(json));
if (prettified.length > 0) {
return prettified;
return {
body: prettified,
headers: responseHeaders,
};
}
} catch (e) {
return responseText;
return {
body: responseText,
headers: responseHeaders,
};
}
}

if (responseText.length > 0) {
return responseText;
return {
body: responseText,
headers: responseHeaders,
};
}
return {
body: '<zero-length response>',
headers: responseHeaders,
};
}

return '<zero-length response>';
protected extractHeaders(headers: Headers): Record<string, string> {
const result: Record<string, string> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}

public findDebugMimeTypeEndpoint(
Expand Down