Repository: codeceptjs/CodeceptJS (2025-08-24T04:35:32Z)

Issue:
REST helper config: `onResponse` provided in config does not work
I am using default REST helper for API testing, and I want to measure and assert the API's response time. For that I am trying following.

In my `codecept.conf.ts`:
```ts
  helpers: {
    REST: {
      endpoint: 'https://myServerUrl',

      onRequest: (request) => {
        console.log('onRequest');
        request.headers[ 'start-time' ] = new Date().getTime();
      },

      onResponse: (response) => {
        console.log('onResponse');
        const startTime = response.request.headers[ 'start-time' ];
        const endTime = new Date().getTime();
        const responseTime = endTime - startTime;
        response. Headers[ 'x-response-time' ] = responseTime;
      },
    },

    JSONResponse: {}
  }
```

and then in my custom helper:
```ts
    const response = this.helpers.JSONResponse.response;
    
    console.log(`Response time: ${response.headers[ 'x-response-time' ]} ms`);
```

I can see that `onRequest` from REST config works as expected but `onResponse` does not work.

My output:
```sh
onRequest
Response time: undefined ms
```


Discussion:
@kobenguyent @DavertMik  - The issue is with the `onResponse ` Handler Overridden in `lib/helper/JSONResponse.js` file

The problem lies in the `lib/helper/JSONResponse.js` file, specifically in the `_beforeSuite()` method.

Currently, the following code unconditionally overrides any existing onResponse handler set in the request helper config:

 ```
 _beforeSuite() {
    this.response = null
    if (!this.helpers[this.options.requestHelper]) {
      throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`)
    }
    // connect to REST helper
    this.helpers[this.options.requestHelper].config.onResponse = response => {
      this.response = response
    }
  }
```

Problem:
This implementation overrides any custom onResponse handler that may have already been set in the user's configuration, causing their logic to be lost.

Suggested Fix:
Preserve the original onResponse handler by wrapping it, rather than replacing it:

```
const originalOnResponse = this.helpers[this.options.requestHelper].config.onResponse
this.helpers[this.options.requestHelper].config.onResponse = response => {
  if (originalOnResponse) originalOnResponse(response)
  this.response = response
}
```

This ensures that both the user-defined handler and the JSONResponse logic are executed.





Patch:
diff --git a/lib/helper/JSONResponse.js b/lib/helper/JSONResponse.js
index 908d4d0bf..bc02f934a 100644
--- a/lib/helper/JSONResponse.js
+++ b/lib/helper/JSONResponse.js
@@ -72,10 +72,11 @@ class JSONResponse extends Helper {
     if (!this.helpers[this.options.requestHelper]) {
       throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`)
     }
-    // connect to REST helper
+    const origOnResponse = this.helpers[this.options.requestHelper].config.onResponse;
     this.helpers[this.options.requestHelper].config.onResponse = response => {
-      this.response = response
-    }
+      this.response = response;
+      if (typeof origOnResponse === 'function') origOnResponse(response);
+    };
   }
 
   _before() {


Test patch:
diff --git a/test/unit/helper/json_response_onResponse_test.js b/test/unit/helper/json_response_onResponse_test.js
new file mode 100644
index 000000000..3fb35108f
--- /dev/null
+++ b/test/unit/helper/json_response_onResponse_test.js
@@ -0,0 +1,68 @@
+const assert = require('assert')
+const REST = require('../../../lib/helper/REST')
+const Container = require('../../../lib/container')
+
+const TestHelper = require('../../support/TestHelper')
+const axios = require('axios')
+const fallBackURL = 'https://jsonplaceholder.typicode.com'
+//start the server using npm run test-server as in the package.json
+let api_url = TestHelper.jsonServerUrl()
+
+describe('REST onResponse Hook Wrapper', () => {
+  let rest
+
+  beforeEach(async () => {
+    Container.helpers({})
+    try {
+      await axios.get(`${api_url}`, { timeout: 1000 }) // Check if the server is reachable
+    } catch (e) {
+      api_url = fallBackURL // Fallback to alternative endpoint
+    }
+
+    rest = new REST({
+      endpoint: api_url,
+      onResponse: res => {
+        res.customFlag = true
+      },
+    })
+
+    rest._before()
+  })
+
+  afterEach(() => {
+    rest = null
+  })
+
+  it('should store response in this.response', async () => {
+    const response = await rest.sendGetRequest('/posts/1')
+    assert.ok(response, 'Expected response to be set on REST instance')
+    assert.equal(response.status, 200)
+  })
+
+  it('should call onResponse function and preserve modifications', async () => {
+    const response = await rest.sendGetRequest('/posts/1')
+    assert.ok(response.customFlag, 'Expected original onResponse to run and modify response')
+  })
+
+  it('should not fail if original onResponse is not set in the config', async () => {
+    const restNoHook = new REST({ endpoint: api_url })
+    restNoHook._before()
+
+    const response = await restNoHook.sendGetRequest('/posts/1')
+    assert.ok(response, 'Expected response to be returned')
+    assert.equal(response.status, 200)
+  })
+
+  it('should not throw if onResponse is not a function in the config', async () => {
+    const restInvalid = new REST({
+      endpoint: api_url,
+      onResponse: undefined,
+    })
+
+    restInvalid._before()
+
+    const response = await restInvalid.sendGetRequest('/posts/1')
+    assert.ok(response)
+    assert.equal(response.status, 200)
+  })
+})