nuxt-open-fetch icon indicating copy to clipboard operation
nuxt-open-fetch copied to clipboard

The request body is not URL encoded if the mime type is "application/x-www-form-urlencoded"

Open xavier-ottolini opened this issue 1 year ago • 0 comments

Environment



Reproduction

I try to create a REST request to perform authentication. The body must be url encoded. My API is the following:

{
 "openapi": "3.1.0",
 "info": { "title": "FastAPI", "version": "0.1.0" },
 "servers": [{ "url": "/auth" }],
 "paths": {
   "/login": {
     "post": {
       "summary": "Auth:Jwt.Login",
       "operationId": "auth_jwt_login_login_post",
       "requestBody": {
         "content": {
           "application/x-www-form-urlencoded": {
             "schema": {
               "$ref": "#/components/schemas/Body_auth_jwt_login_login_post"
             }
           }
         },
         "required": true
       },
       "responses": {
         "200": {
           "description": "Successful Response",
           "content": {
             "application/json": {
               "schema": { "$ref": "#/components/schemas/BearerResponse" },
               "example": {
                 "access_token": "ey...",
                 "token_type": "bearer"
               }
             }
           }
         },
         "400": {
           "description": "Bad Request",
           "content": {
             "application/json": {
               "schema": { "$ref": "#/components/schemas/ErrorModel" },
               "examples": {
                 "LOGIN_BAD_CREDENTIALS": {
                   "summary": "Bad credentials or the user is inactive.",
                   "value": { "detail": "LOGIN_BAD_CREDENTIALS" }
                 },
                 "LOGIN_USER_NOT_VERIFIED": {
                   "summary": "The user is not verified.",
                   "value": { "detail": "LOGIN_USER_NOT_VERIFIED" }
                 }
               }
             }
           }
         },
         "422": {
           "description": "Validation Error",
           "content": {
             "application/json": {
               "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
             }
           }
         }
       }
     }
   }
 },
 "components": {
   "schemas": {
     "BearerResponse": {
       "properties": {
         "access_token": { "type": "string", "title": "Access Token" },
         "token_type": { "type": "string", "title": "Token Type" }
       },
       "type": "object",
       "required": ["access_token", "token_type"],
       "title": "BearerResponse"
     },
     "Body_auth_jwt_login_login_post": {
       "properties": {
         "grant_type": {
           "anyOf": [
             { "type": "string", "pattern": "password" },
             { "type": "null" }
           ],
           "title": "Grant Type"
         },
         "username": { "type": "string", "title": "Username" },
         "password": { "type": "string", "title": "Password" },
         "scope": { "type": "string", "title": "Scope", "default": "" },
         "client_id": {
           "anyOf": [{ "type": "string" }, { "type": "null" }],
           "title": "Client Id"
         },
         "client_secret": {
           "anyOf": [{ "type": "string" }, { "type": "null" }],
           "title": "Client Secret"
         }
       },
       "type": "object",
       "required": ["username", "password"],
       "title": "Body_auth_jwt_login_login_post"
     },
     "ValidationError": {
       "properties": {
         "loc": {
           "items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
           "type": "array",
           "title": "Location"
         },
         "msg": { "type": "string", "title": "Message" },
         "type": { "type": "string", "title": "Error Type" }
       },
       "type": "object",
       "required": ["loc", "msg", "type"],
       "title": "ValidationError"
     }
   }
 }
}

The generated API client with Nuxt Open Fetch is the following:

export interface paths {
  "/login": {
    parameters: {
      query?: never;
      header?: never;
      path?: never;
      cookie?: never;
    };
    get?: never;
    put?: never;
    /** Auth:Jwt.Login */
    post: operations["auth_jwt_login_login_post"];
    delete?: never;
    options?: never;
    head?: never;
    patch?: never;
    trace?: never;
  };
}
export type webhooks = Record<string, never>;
export interface operations {
  auth_jwt_login_login_post: {
    parameters: {
      query?: never;
      header?: never;
      path?: never;
      cookie?: never;
    };
    requestBody: {
      content: {
        "application/x-www-form-urlencoded": components["schemas"]["Body_auth_jwt_login_login_post"];
      };
    };
    responses: {
      /** @description Successful Response */
      200: {
        headers: {
          [name: string]: unknown;
        };
        content: {
          "application/json": components["schemas"]["BearerResponse"];
        };
      };
      /** @description Bad Request */
      400: {
        headers: {
          [name: string]: unknown;
        };
        content: {
          "application/json": components["schemas"]["ErrorModel"];
        };
      };
      /** @description Validation Error */
      422: {
        headers: {
          [name: string]: unknown;
        };
        content: {
          "application/json": components["schemas"]["HTTPValidationError"];
        };
      };
    };
  };
}
export interface components {
  schemas: {
    /** BearerResponse */
    BearerResponse: {
      /** Access Token */
      access_token: string;
      /** Token Type */
      token_type: string;
    };
    /** Body_auth_jwt_login_login_post */
    Body_auth_jwt_login_login_post: {
      /** Grant Type */
      grant_type?: string | null;
      /** Username */
      username: string;
      /** Password */
      password: string;
      /**
       * Scope
       * @default
       */
      scope: string;
      /** Client Id */
      client_id?: string | null;
      /** Client Secret */
      client_secret?: string | null;
    };
    /** ErrorModel */
    ErrorModel: {
      /** Detail */
      detail: string | {
        [key: string]: string | undefined;
      };
    };
    /** ValidationError */
    ValidationError: {
      /** Location */
      loc: (string | number)[];
      /** Message */
      msg: string;
      /** Error Type */
      type: string;
    };
    /** HTTPValidationError */
    HTTPValidationError: {
      /** Detail */
      detail?: components["schemas"]["ValidationError"][];
    };
  };
}

I have developped a Pinia store to rend the request login. As follow

import type { components as AuthComponents } from '#open-fetch-schemas/auth'
import type { AuthenticationStateI } from '~/utils/types/AuthenticationState.interface';
import type { UserJwtAutenticiation } from '~/utils/types/UserAutentication.interface';
import { FetchError } from "ofetch";

export const authenticationStore = defineStore('authenticationStore', {
  /**
   * 
   * @returns 
   */
  state: (): AuthenticationStateI => ({
    bearer: undefined,
    authenticated: false

  }),
  actions: {
    async login(userAuthentication: AuthComponents["schemas"]["Body_auth_jwt_login_login_post"]): Promise<FetchError | undefined> {
      switch (userAuthentication.grant_type) {
        case 'password':
          return await this.passwordLogin(userAuthentication)
        default:
          return Promise.reject(new FetchError('Unknown user authentication grant type'))
      }
    },

    async passwordLogin(userAuthentication: AuthComponents["schemas"]["Body_auth_jwt_login_login_post"]): Promise<FetchError | undefined> {
      const { data, error, status } = await useAuth('/login', {
        method: 'post',
        body: userAuthentication,
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        watch: false,
      })

      if (data.value && status.value !== 'error') {
        this.bearer = data.value
        this.authenticated = true
      } else {
        this.bearer = null
        this.authenticated = false
      }

      return error.value as FetchError | undefined
    }
  }
})

Describe the bug

The login method, the body is in JSON and not URL encoded. The request header is

POST /auth/login HTTP/1.1
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 91
Host: localhost:7000
Origin: http://localhost:3000
Pragma: no-cache
Referer: http://localhost:3000/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
accept: application/json
content-type: application/x-www-form-urlencoded
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

The request body is in JSON format

{
  "username": "[email protected]",
  "password": "***",
  "scope": "",
  "grant_type": "password"
}

As the content type is "application/x-www-form-urlencoded", the expected body should be:

grant_type=password&username=me%mydomain.com&password=***

The response has as status 422. The response body is :

{
    "detail": [
        {
            "type": "missing",
            "loc": [
                "body",
                "username"
            ],
            "msg": "Field required",
            "input": null
        },
        {
            "type": "missing",
            "loc": [
                "body",
                "password"
            ],
            "msg": "Field required",
            "input": null
        }
    ]
}

If y send an URL encoded string instead of an object in the body, the IDE linter detects that the format is not as expected.

NuxtOpenFetch API should parse the data in url encoded according to the mime type.

Additional context

No response

Logs

No response

xavier-ottolini avatar Sep 16 '24 07:09 xavier-ottolini