loopbackio / loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.
https://loopback.io
Other
4.93k stars 1.06k forks source link

Unsupproted Media Type Error with application/json-patch+json #7617

Open emanual4real opened 3 years ago

emanual4real commented 3 years ago

Steps to reproduce

  1. Run application
  2. Send patch request with application/json-patch+json media type

Current Behavior

UnsupportedMediaTypeError: Content-type application/json-patch+json does not match [application/json]

Expected Behavior

I expect the application to accept application/json-patch+json media type

Link to reproduction sandbox

Work won't allow me to put code in sandbox.

Additional information

win32 x64 14.16.0 +-- @loopback/authentication@7.3.1 +-- @loopback/authentication-passport@3.2.1 +-- @loopback/boot@3.4.1 +-- @loopback/core@2.16.1 +-- @loopback/repository@3.7.0 +-- @loopback/rest@9.3.1 +-- @loopback/rest-explorer@3.3.1 +-- @loopback/security@0.5.1 +-- @loopback/service-proxy@3.2.1 +-- UNMET DEPENDENCY @opsdash/loopback-okta-auth@^0.0.1npm +-- @raad/loopback-logging@2.0.1 +-- @raad/loopback-problems@1.0.1ERR! peer dep missing: eslint-plugin-spellcheck@^0.0.17, required by @raad/eslint-config-codestyle-typescript@5.0.5 npm+-- loopback-connector-mongodb@6.0.1 ERR! missing: @opsdash/loopback-okta-auth@^0.0.1, required by @opsdash/user-service@0.0.1

specific controller

  @patch('/users/me')
  @response(200, {
    description: 'User model instance',
    content: { 'application/json': { schema: getModelSchemaRef(User) } },
  })
  @authenticate(OKTA_AUTH_TOKEN_STRATEGY_NAME)
  async updateUser(
    @requestBody({
      description: 'User model instance',
      required: true,
      content: {
        'application/json-patch+json': {
          schema: {
            type: 'array',
            items: getModelSchemaRef(JsonPatch),
          },
        },
      },
    })
    patches: JsonPatch[],
  ): Promise<User> {
    let user = await this.userRepository.findOne({
      where: { key: this.userProfile.id },
    });

    if (!user) {
      throw new HttpErrors.NotFound();
    }

    await this.userRepository.jsonPatch(user, patches);

    user = await this.userRepository.findOne({
      where: { key: this.userProfile.id },
    });

    if (!user) {
      throw new HttpErrors.NotFound();
    }

    return user;
  }

JsonPatch model

import { Model, model, property } from '@loopback/repository';
import { JSONValue } from '@loopback/core';

@model()
export class JsonPatch extends Model {
  @property({
    type: 'string',
    required: true,
    jsonSchema: {
      enum: ['add', 'remove', 'replace', 'move', 'copy', 'test', '_get'],
    },
  })
  op: 'add' & 'remove' & 'replace' & 'move' & 'copy' & 'test' & '_get';

  @property({
    type: 'string',
    required: true,
  })
  path: string;

  @property({
    type: 'object',
  })
  value?: JSONValue;

  @property({
    type: 'string',
  })
  from?: string;

  constructor(data?: Partial<JsonPatch>) {
    super(data);
  }
}

export interface JsonPatchRelations {
  // describe navigational properties here
}

export type JsonPatchWithRelations = JsonPatch & JsonPatchRelations;

error stack

[2021-07-14T14:03:02.131Z] ERROR (UserService/4892 on CHRDEV42725):
    requestId: "RequestContext-QuzkmMDRTbiTuMkFzvMHAA-19"
    correlationId: "QuzkmMDRTbiTuMkFzvMHAA-20"
    event: "rest.sequence.actions.reject"
    err: {
      "type": "UnprocessableEntityError",
      "message": "The request body is invalid. See error object `details` property for more info.",
      "stack":
          UnprocessableEntityError: The request body is invalid. See error object `details` property for more info.
              at Object.invalidRequestBody (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\rest-http-error.ts:59:7)
              at validateValueAgainstSchema (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\validation\request-body.validator.ts:182:34)   
              at processTicksAndRejections (internal/process/task_queues.js:93:5)
              at Object.validateRequestBody (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\validation\request-body.validator.ts:75:3)     
              at buildOperationArguments (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\parser.ts:92:3)
      "code": "VALIDATION_FAILED",
      "details": [
        {
          "path": "/0/value",
          "code": "type",
          "message": "should be object",
          "info": {
            "type": "object"
          }
        },
        {
          "path": "/1/value",
          "code": "type",
          "message": "should be object",
          "info": {
            "type": "object"
          }
        },
        {
          "path": "/2/value",
          "code": "type",
          "message": "should be object",
          "info": {
            "type": "object"
          }
        }
      ],
      "status": 422,
      "statusCode": 422,
      "expose": true
    }
[2021-07-14T14:03:02.144Z] INFO (UserService/4892 on CHRDEV42725):
    requestId: "RequestContext-QuzkmMDRTbiTuMkFzvMHAA-19"
    correlationId: "QuzkmMDRTbiTuMkFzvMHAA-20"
    user: {
      "id": "00u14kjbo3IK5P8Fp5d7",
      "groups": []
    }
    req: {
      "method": "PATCH",
      "url": "/users/me",
      "query": {},
      "params": {},
      "headers": {
        "host": "localhost:8080",
        "connection": "keep-alive",
        "content-length": "171",
        "sec-ch-ua": "\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"",
        "authorization": "[Redacted]",
        "sec-ch-ua-mobile": "?0",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
        "content-type": "application/json-patch+json",
        "accept": "*/*",
        "origin": "http://localhost:3001",
        "sec-fetch-site": "same-site",
        "sec-fetch-mode": "cors",
        "sec-fetch-dest": "empty",
        "referer": "http://localhost:3001/",
        "accept-encoding": "gzip, deflate, br",
        "accept-language": "en-US,en;q=0.9",
        "x-correlation-id": "QuzkmMDRTbiTuMkFzvMHAA-20"
      },
      "remoteAddress": "::1",
      "remotePort": 63286
    }
    res: {
      "statusCode": 422,
      "headers": {
        "x-powered-by": "Express",
        "x-correlation-id": "QuzkmMDRTbiTuMkFzvMHAA-20",
        "access-control-allow-origin": "*",
        "access-control-allow-credentials": "true",
        "content-type": "application/problem+json; charset=utf-8",
        "content-length": "201",
        "etag": "W/\"c9-KXBlVP+3LvkN9OBVXa4YHIp4j2c\""
      }
    }
    duration: 156.0314
    event: "access"

Request body

[{"op":"add","path":"/learningStyle","value":"Intuitive"},{"op":"add","path":"/recognitionStyle","value":"Public"},{"op":"add","path":"/activityStyle","value":"Hands On"}]
emanual4real commented 3 years ago

My fault, been playing with a couple variations. If I modify the controller to requestBody.array() I get the 415 error instead of 422.

    @requestBody.array({
      description: 'User model instance',
      content: {
        'application/json-patch+json': {
          schema: {
            type: 'array',
            items: getModelSchemaRef(JsonPatch),
          },
        },
        required: true,
      },
    })

stack error

[2021-07-14T14:09:44.102Z] ERROR (UserService/21200 on CHRDEV42725):
    requestId: "RequestContext-57g6pfjtTD_EV-UkMD2NZQ-6"
    correlationId: "57g6pfjtTD_EV-UkMD2NZQ-7"
    event: "rest.sequence.actions.reject"
    err: {
      "type": "UnsupportedMediaTypeError",
      "message": "Content-type application/json-patch+json does not match [application/json].",
      "stack":
          UnsupportedMediaTypeError: Content-type application/json-patch+json does not match [application/json].
              at Object.unsupportedMediaType (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\rest-http-error.ts:32:26)
              at RequestBodyParser._matchRequestBodySpec (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\body-parsers\body-parser.ts:129:28)
              at RequestBodyParser.loadRequestBodyIfNeeded (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\body-parsers\body-parser.ts:54:52)
              at Object.parseOperationArgs (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\parser.ts:44:40)
              at D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\providers\parse-params.provider.ts:68:28
              at processTicksAndRejections (internal/process/task_queues.js:93:5)
              at D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\express\src\middleware-interceptor.ts:131:19
              at D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\express\src\middleware-interceptor.ts:131:19
              at D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\providers\send.provider.ts:46:24
              at MySequence.handle (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\sequence.ts:291:5)
              at HttpHandler._handleRequest (D:\Workspace\javascript\opsdash-services-test\opsdash-services\packages\user-service\node_modules\@loopback\rest\src\http-handler.ts:115:5)
      "code": "UNSUPPORTED_MEDIA_TYPE",
      "contentType": "application/json-patch+json",
      "allowedMediaTypes": [
        "application/json"
      ],
      "status": 415,
      "statusCode": 415,
      "expose": true
    }
[2021-07-14T14:09:44.136Z] INFO (UserService/21200 on CHRDEV42725):
    requestId: "RequestContext-57g6pfjtTD_EV-UkMD2NZQ-6"
    correlationId: "57g6pfjtTD_EV-UkMD2NZQ-7"
    user: {
      "id": "00u14kjbo3IK5P8Fp5d7",
      "groups": []
    }
    req: {
      "method": "PATCH",
      "url": "/users/me",
      "query": {},
      "params": {},
      "headers": {
        "host": "localhost:8080",
        "connection": "keep-alive",
        "content-length": "126",
        "sec-ch-ua": "\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"",
        "authorization": "[Redacted]",
        "sec-ch-ua-mobile": "?0",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
        "content-type": "application/json-patch+json",
        "accept": "*/*",
        "origin": "http://localhost:3001",
        "sec-fetch-site": "same-site",
        "sec-fetch-mode": "cors",
        "sec-fetch-dest": "empty",
        "referer": "http://localhost:3001/",
        "accept-encoding": "gzip, deflate, br",
        "accept-language": "en-US,en;q=0.9",
        "x-correlation-id": "57g6pfjtTD_EV-UkMD2NZQ-7"
      },
      "remoteAddress": "::1",
      "remotePort": 59861
    }
    res: {
      "statusCode": 415,
      "headers": {
        "x-powered-by": "Express",
        "x-correlation-id": "57g6pfjtTD_EV-UkMD2NZQ-7",
        "access-control-allow-origin": "*",
        "access-control-allow-credentials": "true",
        "content-type": "application/problem+json; charset=utf-8",
        "content-length": "152",
        "etag": "W/\"98-MRnxNanuvSuM0nDMJLXs3JKTDdI\""
      }
    }
    duration: 39.035
    event: "access"