Repository: hyperjumptech/monika
Pull request #1293 (2024-05-16 08:21:34)

Fix: Monika sends null request body as string null
# Monika Pull Request (PR)

## What feature/issue does this PR add

Fix Monika sends `null` request body as `"null"`.

## How did you implement / how did you fix it

1. Add a test.
2. Change the request body type.
3. Remove the non string body conversion using `JSON.stringify` that caused `null` becomes `"null"`.

## How to test

Run Monika with the following configuration:

```yaml
probes:
  - id: '1'
    interval: 1
    requests:
      - url: https://idp.sinarmas.com/api/health
        body: null
```

## Demo
[output.webm](https://github.com/hyperjumptech/monika/assets/15191978/64f5bae5-247a-4fc6-8764-213da6207519)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/hyperjumptech/monika/pull/1293?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) Report
Attention: Patch coverage is `88.88889%` with `1 lines` in your changes are missing coverage. Please review.
> Project coverage is 63.89%. Comparing base [(`6a29470`)](https://app.codecov.io/gh/hyperjumptech/monika/commit/6a2947073e1935f0fef6c1b93fb4784295f343f6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) to head [(`1032cfa`)](https://app.codecov.io/gh/hyperjumptech/monika/pull/1293?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech).
> Report is 14 commits behind head on main.

> :exclamation: Current head 1032cfa differs from pull request most recent head 15f4127. Consider uploading reports for the commit 15f4127 to get more accurate results

| [Files](https://app.codecov.io/gh/hyperjumptech/monika/pull/1293?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) | Patch % | Lines |
|---|---|---|
| [src/components/probe/prober/http/request.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1293?src=pr&el=tree&filepath=src%2Fcomponents%2Fprobe%2Fprober%2Fhttp%2Frequest.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvcHJvYmUvcHJvYmVyL2h0dHAvcmVxdWVzdC50cw==) | 88.88% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/hyperjumptech/monika/pull/1293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1293      +/-   ##
==========================================
+ Coverage   62.51%   63.89%   +1.37%     
==========================================
  Files         112      108       -4     
  Lines        3391     3376      -15     
  Branches      591      572      -19     
==========================================
+ Hits         2120     2157      +37     
+ Misses       1079     1036      -43     
+ Partials      192      183       -9     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/hyperjumptech/monika/pull/1293?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech).


Patch:
diff --git a/src/components/config/parser/text.ts b/src/components/config/parser/text.ts
index 3558d24e..99ec6de3 100644
--- a/src/components/config/parser/text.ts
+++ b/src/components/config/parser/text.ts
@@ -67,7 +67,6 @@ export const parseConfigFromText = (configString: string): Config => {
               url,
               method: 'GET',
               timeout: 10_000,
-              body: {} as JSON,
               followRedirects: getContext().flags['follow-redirects'],
             },
           ],
diff --git a/src/components/probe/prober/http/request.ts b/src/components/probe/prober/http/request.ts
index 6b964d11..dc506bfa 100644
--- a/src/components/probe/prober/http/request.ts
+++ b/src/components/probe/prober/http/request.ts
@@ -27,7 +27,7 @@ import FormData from 'form-data'
 import Joi from 'joi'
 // eslint-disable-next-line no-restricted-imports
 import * as qs from 'querystring'
-import { errors as undiciErrors } from 'undici'
+import { type BodyInit, errors as undiciErrors } from 'undici'
 import YAML from 'yaml'
 import {
   type ProbeRequestResponse,
@@ -73,17 +73,17 @@ export async function httpRequest({
     ping,
     allowUnauthorized,
     followRedirects,
+    signal,
   } = requestConfig
-  const newReq = { method, headers, timeout, body, ping }
+  const newReq = { method, headers, timeout, body, ping, signal }
   const renderURL = Handlebars.compile(url)
   const renderedURL = renderURL({ responses })
-  newReq.headers = compileHeaders(headers, body, responses as never)
   // compile body needs to modify headers if necessary
-  const { headers: newHeaders, body: newBody } = compileBody(
-    newReq.headers,
+  const { headers: newHeaders, body: newBody } = compileBody({
+    responses,
     body,
-    responses
-  )
+    headers: compileHeaders({ headers, responses, body }),
+  })
   newReq.headers = newHeaders
   newReq.body = newBody
 
@@ -149,11 +149,13 @@ export async function httpRequest({
   }
 }
 
-function compileHeaders(
-  headers: object | undefined,
-  body: string | object,
-  responses: never
-) {
+type ChainingRequest = {
+  responses: Array<ProbeRequestResponse>
+  body?: BodyInit
+  headers?: object
+}
+
+function compileHeaders({ headers, responses, body }: ChainingRequest) {
   // return as-is if falsy
   if (!headers) return headers
   // Compile headers using handlebars to render URLs that uses previous responses data.
@@ -193,18 +195,18 @@ function compileHeaders(
   return newHeaders
 }
 
-function compileBody(
-  headers: object | undefined,
-  body: object | string,
-  responses: ProbeRequestResponse[]
-): {
-  headers: object | undefined
-  body: object | string
-} {
+function compileBody({
+  responses,
+  body,
+  headers,
+}: ChainingRequest): Pick<ChainingRequest, 'body' | 'headers'> {
   // return as-is if falsy
   if (!body) return { headers, body }
   let newHeaders = headers
-  let newBody = generateRequestChainingBody(body, responses)
+  let newBody: BodyInit | undefined = generateRequestChainingBody(
+    body,
+    responses
+  )
 
   if (newHeaders) {
     const contentTypeKey = Object.keys(newHeaders || {}).find(
@@ -247,7 +249,7 @@ async function probeHttpFetch({
     method: string | undefined
     headers: Headers | undefined
     timeout: number
-    body: string | object
+    body?: BodyInit
     ping: boolean | undefined
   }
 }): Promise<ProbeRequestResponse> {
@@ -302,7 +304,7 @@ type ProbeHTTPAxiosParams = {
     method: string | undefined
     headers: Headers | undefined
     timeout: number
-    body: string | object
+    body?: BodyInit
     ping: boolean | undefined
   }
 }
@@ -320,10 +322,6 @@ async function probeHttpAxios({
     keepalive: true,
     url: renderedURL,
     maxRedirects,
-    body:
-      typeof requestParams.body === 'string'
-        ? requestParams.body
-        : JSON.stringify(requestParams.body),
   })
 
   const responseTime = Date.now() - startTime
@@ -343,7 +341,7 @@ async function probeHttpAxios({
 export function generateRequestChainingBody(
   body: object | string,
   responses: ProbeRequestResponse[]
-): object | string {
+): BodyInit {
   const isString = typeof body === 'string'
   const template = Handlebars.compile(isString ? body : JSON.stringify(body))
   const renderedBody = template({ responses })
@@ -352,9 +350,13 @@ export function generateRequestChainingBody(
 }
 
 function transformContentByType(
-  content: object | string,
+  content?: BodyInit,
   contentType?: string | number | boolean
 ) {
+  if (!content) {
+    return { content, contentType }
+  }
+
   switch (contentType) {
     case 'application/json': {
       return { content: JSON.stringify(content), contentType }
@@ -369,9 +371,8 @@ function transformContentByType(
 
     case 'multipart/form-data': {
       const form = new FormData()
-
       for (const contentKey of Object.keys(content)) {
-        form.append(contentKey, (content as Record<string, never>)[contentKey])
+        form.append(contentKey, (content as any)[contentKey])
       }
 
       return { content: form, contentType: form.getHeaders()['content-type'] }
diff --git a/src/interfaces/request.ts b/src/interfaces/request.ts
index 03931e12..e588f206 100644
--- a/src/interfaces/request.ts
+++ b/src/interfaces/request.ts
@@ -56,11 +56,10 @@ export interface ProbeRequestResponse<T = unknown> {
 }
 
 // ProbeRequest is used to define the requests that is being made.
-export interface RequestConfig extends Omit<RequestInit, 'headers' | 'body'> {
+export interface RequestConfig extends Omit<RequestInit, 'headers'> {
   id?: string
   saveBody?: boolean // save response body to db?
   url: string
-  body: object | string
   followRedirects: number
   timeout: number // request timeout
   alerts?: ProbeAlert[]


Test patch:
diff --git a/src/components/config/__tests__/expected.textfile.yml b/src/components/config/__tests__/expected.textfile.yml
index c1c19fe0..4dea12fe 100644
--- a/src/components/config/__tests__/expected.textfile.yml
+++ b/src/components/config/__tests__/expected.textfile.yml
@@ -5,7 +5,6 @@ probes:
       - url: https://monika.hyperjump.tech
         method: GET
         timeout: 10000
-        body: {}
         followRedirects: 21
     interval: 900
     alerts:
@@ -19,7 +18,6 @@ probes:
       - url: https://github.com
         method: GET
         timeout: 10000
-        body: {}
         followRedirects: 21
     interval: 900
     alerts:
diff --git a/src/components/probe/prober/http/request.test.ts b/src/components/probe/prober/http/request.test.ts
index 5ba42899..0f768629 100644
--- a/src/components/probe/prober/http/request.test.ts
+++ b/src/components/probe/prober/http/request.test.ts
@@ -537,6 +537,34 @@ describe('probingHTTP', () => {
       // assert
       expect(res.status).to.eq(302)
     })
+
+    it('should send no body', async () => {
+      // arrange
+      let body
+      server.use(
+        http.get('https://example.com', async ({ request }) => {
+          body = request.body
+
+          return new HttpResponse(null, {
+            status: 200,
+          })
+        })
+      )
+      const request = {
+        url: 'https://example.com',
+        body: null,
+      } as RequestConfig
+
+      // act
+      const res = await httpRequest({
+        requestConfig: request,
+        responses: [],
+      })
+
+      // assert
+      expect(res.status).to.eq(200)
+      expect(body).to.eq(null)
+    })
   })
 
   describe('Unit test', () => {
diff --git a/src/symon/index.test.ts b/src/symon/index.test.ts
index bb04352d..ae2aa3b4 100644
--- a/src/symon/index.test.ts
+++ b/src/symon/index.test.ts
@@ -206,7 +206,7 @@ describe('Symon initiate', () => {
         })
       ),
       http.get('http://localhost:4000/api/v1/monika/1234/probes', () => {
-        throw new Error('Failed')
+        throw new Error('Failed to get probes from Symon')
       })
     )