Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc0a2b5f

Browse files
authored
Fix JSON.stringify circular reference error in logging infrastructure (#607)
Resolves circular structure errors when serializing request/response bodies for logging.Other changes:* Added test coverage for logging infrastructure* Fixed flaky stream test* Calculate body size directly from strings/buffers instead of stringifying* Use util.inspect instead of JSON.stringify for body serializationFixes#606
1 parentb2ac27b commitc0a2b5f

File tree

14 files changed

+573
-59
lines changed

14 files changed

+573
-59
lines changed

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
###Fixed
66

77
- Logging in or out in one VS Code window now properly updates the authentication status in all other open windows.
8+
- Fix an issue with JSON stringification errors occurring when logging circular objects.
89

910
###Added
1011

‎src/api/coderApi.ts‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import{typeAxiosInstance}from"axios";
1+
import{
2+
typeAxiosResponseHeaders,
3+
typeAxiosInstance,
4+
typeAxiosHeaders,
5+
typeAxiosResponseTransformer,
6+
}from"axios";
27
import{Api}from"coder/site/src/api/api";
38
import{
49
typeGetInboxNotificationResponse,
@@ -23,6 +28,7 @@ import {
2328
typeRequestConfigWithMeta,
2429
HttpClientLogLevel,
2530
}from"../logging/types";
31+
import{sizeOf}from"../logging/utils";
2632
import{WsLogger}from"../logging/wsLogger";
2733
import{
2834
OneWayWebSocket,
@@ -207,7 +213,24 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
207213
(config)=>{
208214
constconfigWithMeta=configasRequestConfigWithMeta;
209215
configWithMeta.metadata=createRequestMeta();
210-
logRequest(logger,configWithMeta,getLogLevel());
216+
217+
config.transformRequest=[
218+
...wrapRequestTransform(
219+
config.transformRequest||client.defaults.transformRequest||[],
220+
configWithMeta,
221+
),
222+
(data)=>{
223+
// Log after setting the raw request size
224+
logRequest(logger,configWithMeta,getLogLevel());
225+
returndata;
226+
},
227+
];
228+
229+
config.transformResponse=wrapResponseTransform(
230+
config.transformResponse||client.defaults.transformResponse||[],
231+
configWithMeta,
232+
);
233+
211234
returnconfig;
212235
},
213236
(error:unknown)=>{
@@ -228,6 +251,59 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
228251
);
229252
}
230253

254+
functionwrapRequestTransform(
255+
transformer:AxiosResponseTransformer|AxiosResponseTransformer[],
256+
config:RequestConfigWithMeta,
257+
):AxiosResponseTransformer[]{
258+
return[
259+
(data:unknown,headers:AxiosHeaders)=>{
260+
consttransformerArray=Array.isArray(transformer)
261+
?transformer
262+
:[transformer];
263+
264+
// Transform the request first then get the size (measure what's sent over the wire)
265+
constresult=transformerArray.reduce(
266+
(d,fn)=>fn.call(config,d,headers),
267+
data,
268+
);
269+
270+
config.rawRequestSize=getSize(config.headers,result);
271+
272+
returnresult;
273+
},
274+
];
275+
}
276+
277+
functionwrapResponseTransform(
278+
transformer:AxiosResponseTransformer|AxiosResponseTransformer[],
279+
config:RequestConfigWithMeta,
280+
):AxiosResponseTransformer[]{
281+
return[
282+
(data:unknown,headers:AxiosResponseHeaders,status?:number)=>{
283+
// Get the size before transforming the response (measure what's sent over the wire)
284+
config.rawResponseSize=getSize(headers,data);
285+
286+
consttransformerArray=Array.isArray(transformer)
287+
?transformer
288+
:[transformer];
289+
290+
returntransformerArray.reduce(
291+
(d,fn)=>fn.call(config,d,headers,status),
292+
data,
293+
);
294+
},
295+
];
296+
}
297+
298+
functiongetSize(headers:AxiosHeaders,data:unknown):number|undefined{
299+
constcontentLength=headers["content-length"];
300+
if(contentLength!==undefined){
301+
returnparseInt(contentLength,10);
302+
}
303+
304+
returnsizeOf(data);
305+
}
306+
231307
functiongetLogLevel():HttpClientLogLevel{
232308
constlogLevelStr=vscode.workspace
233309
.getConfiguration()

‎src/core/cliManager.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ export class CliManager {
267267
if(Number.isNaN(contentLength)){
268268
this.output.warn(
269269
"Got invalid or missing content length",
270-
rawContentLength,
270+
rawContentLength??"",
271271
);
272272
}else{
273273
this.output.info("Got content length",prettyBytes(contentLength));

‎src/logging/formatters.ts‎

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
importprettyBytesfrom"pretty-bytes";
22

3-
importtype{InternalAxiosRequestConfig}from"axios";
3+
import{safeStringify}from"./utils";
4+
5+
importtype{AxiosRequestConfig}from"axios";
46

57
constSENSITIVE_HEADERS=["Coder-Session-Token","Proxy-Authorization"];
68

@@ -18,35 +20,14 @@ export function formatTime(ms: number): string {
1820
}
1921

2022
exportfunctionformatMethod(method:string|undefined):string{
21-
return(method??"GET").toUpperCase();
23+
returnmethod?.toUpperCase()||"GET";
2224
}
2325

24-
/**
25-
* Formats content-length for display. Returns the header value if available,
26-
* otherwise estimates size by serializing the data body (prefixed with ~).
27-
*/
28-
exportfunctionformatContentLength(
29-
headers:Record<string,unknown>,
30-
data:unknown,
31-
):string{
32-
constlen=headers["content-length"];
33-
if(len&&typeoflen==="string"){
34-
constbytes=parseInt(len,10);
35-
returnisNaN(bytes) ?"(?b)" :`(${prettyBytes(bytes)})`;
36-
}
37-
38-
// Estimate from data if no header
39-
if(data!==undefined&&data!==null){
40-
constestimated=Buffer.byteLength(JSON.stringify(data),"utf8");
41-
return`(~${prettyBytes(estimated)})`;
42-
}
43-
44-
return`(${prettyBytes(0)})`;
26+
exportfunctionformatSize(size:number|undefined):string{
27+
returnsize===undefined ?"(? B)" :`(${prettyBytes(size)})`;
4528
}
4629

47-
exportfunctionformatUri(
48-
config:InternalAxiosRequestConfig|undefined,
49-
):string{
30+
exportfunctionformatUri(config:AxiosRequestConfig|undefined):string{
5031
returnconfig?.url||"<no url>";
5132
}
5233

@@ -66,7 +47,7 @@ export function formatHeaders(headers: Record<string, unknown>): string {
6647

6748
exportfunctionformatBody(body:unknown):string{
6849
if(body){
69-
returnJSON.stringify(body);
50+
returnsafeStringify(body)??"<invalid body>";
7051
}else{
7152
return"<no body>";
7253
}

‎src/logging/httpLogger.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { getErrorDetail } from "../error";
55

66
import{
77
formatBody,
8-
formatContentLength,
98
formatHeaders,
109
formatMethod,
10+
formatSize,
1111
formatTime,
1212
formatUri,
1313
}from"./formatters";
@@ -42,11 +42,10 @@ export function logRequest(
4242
return;
4343
}
4444

45-
const{ requestId, method, url}=parseConfig(config);
46-
constlen=formatContentLength(config.headers,config.data);
45+
const{ requestId, method, url, requestSize}=parseConfig(config);
4746

4847
constmsg=[
49-
`→${shortId(requestId)}${method}${url}${len}`,
48+
`→${shortId(requestId)}${method}${url}${requestSize}`,
5049
...buildExtraLogs(config.headers,config.data,logLevel),
5150
];
5251
logger.trace(msg.join("\n"));
@@ -64,11 +63,12 @@ export function logResponse(
6463
return;
6564
}
6665

67-
const{ requestId, method, url, time}=parseConfig(response.config);
68-
constlen=formatContentLength(response.headers,response.data);
66+
const{ requestId, method, url, time, responseSize}=parseConfig(
67+
response.config,
68+
);
6969

7070
constmsg=[
71-
`←${shortId(requestId)}${response.status}${method}${url}${len}${time}`,
71+
`←${shortId(requestId)}${response.status}${method}${url}${responseSize}${time}`,
7272
...buildExtraLogs(response.headers,response.data,logLevel),
7373
];
7474
logger.trace(msg.join("\n"));
@@ -150,12 +150,16 @@ function parseConfig(config: RequestConfigWithMeta | undefined): {
150150
method:string;
151151
url:string;
152152
time:string;
153+
requestSize:string;
154+
responseSize:string;
153155
}{
154156
constmeta=config?.metadata;
155157
return{
156158
requestId:meta?.requestId||"unknown",
157159
method:formatMethod(config?.method),
158160
url:formatUri(config),
159161
time:meta ?formatTime(Date.now()-meta.startedAt) :"?ms",
162+
requestSize:formatSize(config?.rawRequestSize),
163+
responseSize:formatSize(config?.rawResponseSize),
160164
};
161165
}

‎src/logging/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ export interface RequestMeta {
1414

1515
exporttypeRequestConfigWithMeta=InternalAxiosRequestConfig&{
1616
metadata?:RequestMeta;
17+
rawRequestSize?:number;
18+
rawResponseSize?:number;
1719
};

‎src/logging/utils.ts‎

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
import{Buffer}from"node:buffer";
22
importcryptofrom"node:crypto";
3+
importutilfrom"node:util";
34

45
exportfunctionshortId(id:string):string{
56
returnid.slice(0,8);
67
}
78

9+
exportfunctioncreateRequestId():string{
10+
returncrypto.randomUUID().replace(/-/g,"");
11+
}
12+
13+
/**
14+
* Returns the byte size of the data if it can be determined from the data's intrinsic properties,
15+
* otherwise returns undefined (e.g., for plain objects and arrays that would require serialization).
16+
*/
817
exportfunctionsizeOf(data:unknown):number|undefined{
918
if(data===null||data===undefined){
1019
return0;
1120
}
12-
if(typeofdata==="string"){
13-
returnBuffer.byteLength(data);
21+
if(typeofdata==="boolean"){
22+
return4;
23+
}
24+
if(typeofdata==="number"){
25+
return8;
1426
}
15-
if(Buffer.isBuffer(data)){
16-
returndata.length;
27+
if(typeofdata==="string"||typeofdata==="bigint"){
28+
returnBuffer.byteLength(data.toString());
1729
}
18-
if(datainstanceofArrayBuffer||ArrayBuffer.isView(data)){
30+
if(
31+
Buffer.isBuffer(data)||
32+
datainstanceofArrayBuffer||
33+
ArrayBuffer.isView(data)
34+
){
1935
returndata.byteLength;
2036
}
2137
if(
@@ -28,6 +44,19 @@ export function sizeOf(data: unknown): number | undefined {
2844
returnundefined;
2945
}
3046

31-
exportfunctioncreateRequestId():string{
32-
returncrypto.randomUUID().replace(/-/g,"");
47+
exportfunctionsafeStringify(data:unknown):string|null{
48+
try{
49+
returnutil.inspect(data,{
50+
showHidden:false,
51+
depth:Infinity,
52+
maxArrayLength:Infinity,
53+
maxStringLength:Infinity,
54+
breakLength:Infinity,
55+
compact:true,
56+
getters:false,// avoid side-effects
57+
});
58+
}catch{
59+
// Should rarely happen but just in case
60+
returnnull;
61+
}
3362
}

‎src/logging/wsLogger.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,6 @@ export class WsLogger {
7777

7878
privateformatBytes():string{
7979
constbytes=prettyBytes(this.byteCount);
80-
returnthis.unknownByteCount ?`>=${bytes}` :bytes;
80+
returnthis.unknownByteCount ?`>=${bytes}` :bytes;
8181
}
8282
}

‎test/mocks/testHelpers.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import{vi}from"vitest";
22
import*asvscodefrom"vscode";
33

4+
import{typeLogger}from"@/logging/logger";
5+
46
/**
57
* Mock configuration provider that integrates with the vscode workspace configuration mock.
68
* Use this to set configuration values that will be returned by vscode.workspace.getConfiguration().
@@ -286,3 +288,13 @@ export class InMemorySecretStorage implements vscode.SecretStorage {
286288
this.listeners.forEach((listener)=>listener(event));
287289
}
288290
}
291+
292+
exportfunctioncreateMockLogger():Logger{
293+
return{
294+
trace:vi.fn(),
295+
debug:vi.fn(),
296+
info:vi.fn(),
297+
warn:vi.fn(),
298+
error:vi.fn(),
299+
};
300+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp