fastify icon indicating copy to clipboard operation
fastify copied to clipboard

feat: Supporting different content-type responses

Open iifawzi opened this issue 2 years ago • 7 comments

Signed-off-by: iifawzi [email protected]

Hi, this feature supports using different content types responses I've added as many tests as possible to handle all possible cases.

Handling https://github.com/fastify/help/issues/621 and https://github.com/fastify/fastify/issues/3902

Checklist

iifawzi avatar Sep 10 '22 23:09 iifawzi

if this got approved I will start looking at how should we update fastify-swagger to support this too

iifawzi avatar Sep 10 '22 23:09 iifawzi

My only concern is how this syntax will be rendered by fastify-swagger and similar modules. Can you check and possibly open a matching PR there too?

I will take a look at fastify-swagger to see how possible the syntax can be rendered

iifawzi avatar Sep 11 '22 12:09 iifawzi

I'm still investigating on the swagger part, the initial thoughts are that the syntax needs to be changed a little bit in order to allow the description and x-response-description properties to be set when different content types are used because it can only be set based on the status code, so having the status code pointing directly to an array wouldn't work.

I'm still making sure of this, if confirmed, I'm proposing to change the syntax to something similar to the sample below. Introducing contentTypes, will make us able to handle the use of multiple type contents appropriately and the use of the statusCode-based properties such as description or anything.

const schema = {
      response: {
        200: {
           description: '....',
           contentTypes: [
                 { 
                      content: 'application/json', 
                      schema: { 
                        id: { type: 'number' }, 
                        name: { type: 'string' } 
                      } 
                },
                {
                  content: 'application/vnd.v1+json',
                  schema: {
                    type: 'array',
                    items: { $ref: 'test' }
                     }
                }
        ],
           
        }
      }
    }

EDIT: I think I can confirm that, it also applies to the headers.

iifawzi avatar Sep 11 '22 15:09 iifawzi

I've opened a PR at fastify-swagger, I'm not aware of any other modules that would need to adapt, let me know if there are any. @mcollina

iifawzi avatar Sep 11 '22 18:09 iifawzi

I do agree, it makes sense, I will work on making it similar to the OpenAPI Specification as much as possible if not the same.

iifawzi avatar Sep 13 '22 05:09 iifawzi

Syntax is now matching the OpenAPI Specification

const schema = {
      response: {
        200: {
          description: 'Response schema that support different content types',
          content: {
            'application/json': {
              schema: {
                name: { type: 'string' },
                image: { type: 'string' },
                address: { type: 'string' }
              }
            },
            'application/vnd.v1+json': {
              schema: {
                type: 'array',
                items: { $ref: 'test' }
              }
            }
          }
        },
        '3xx': {
          content: {
            'application/vnd.v2+json': {
              schema: {
                fullName: { type: 'string' },
                phone: { type: 'string' }
              }
            }
          }
        }
      }
    }

iifawzi avatar Sep 13 '22 08:09 iifawzi

are there any updates regarding this?

iifawzi avatar Sep 22 '22 09:09 iifawzi

@Uzlopak @Eomm PTAL

mcollina avatar Sep 23 '22 08:09 mcollina

@mcollina

Please see the discussion regarding deprecating the "short-form" https://github.com/fastify/fastify/pull/4264#discussion_r977505336

Uzlopak avatar Sep 23 '22 08:09 Uzlopak

I love this feature! I have implemented a plugin that implements the same functionality with constraints but requires a separate route for each different content type.

Since the schema selection happens based on the content type header, should it be documented in the documentation that the user is responsible for correctly processing the Accept header and setting the appropriate content type header?

matthyk avatar Sep 23 '22 13:09 matthyk

Just summing this up, last commit includes, changing the use of For in to For of, allowing the default schema to benefit from the feature:

    schema: {
      response: {
        default: {
          content: {
            'application/json': {
              schema: {
                details: { type: 'string' }
              }
            }
          }
        }
      }
    }

Adding the contentType to the compile arguments:

    for (const mediaName of Object.keys(schema.content)) {
          const contentSchema = schema.content[mediaName].schema
          contentTypesSchemas[mediaName] = compile({
            schema: contentSchema,
            url,
            method,
            httpStatus: statusCode,
            contentType: mediaName
          })
        }

I still need to document that the contentType option is now exposed, somewhere in the docs.

Tests are added to ensure the additions.

iifawzi avatar Sep 24 '22 13:09 iifawzi

@Eomm PTAL

mcollina avatar Oct 02 '22 09:10 mcollina

@Uzlopak mind taking a gander when you get a spare second as well, please?

Fdawgs avatar Oct 04 '22 08:10 Fdawgs

I write this here and not in the comment above because I think we need all interested contributors to give feedback. So especially @mcollina @Eomm @jsumners @Fdawgs @climba03003

First of all @iifawzi did a great job and this PR is already processed for about a month. So dont get demotivated.

The current implementation is focussing on the fastify-swagger integration. It makes sense in the way, that other content type responses can be declared in the schema.

But in my personal opinion this PR is lacking the aspect that we have to integrate it properly into fastify-core.

I think the issue is best described by the unit test, which @iifawzi provided. See https://github.com/fastify/fastify/blob/6b31fbf948873d5f0d809501b09796a8b9ce7926/test/schema-serialization.test.js#L125

So what happens is, that the serialization is done in the handler. Which is imho an anti pattern. The unit tests is also just working as expected because all of the content types are variations of json.

What we actually need is an equivalent to the ContentTypeParser but for Serializiation.

This is what I would expect on the fastify-core part:

const Fastify = require('fastify')
const xmlSerializer = require('some-randowm-xml-serializer')

const fastify = new Fastify()
fastify.addContentTypeSerializer('text/xml', {}, xmlSerializer)

  fastify.get('/', {
    schema: {
      response: {
        200: {
          content: {
            'application/json': {
              schema: {
                name: { type: 'string' },
                image: { type: 'string' },
                address: { type: 'string' }
              }
            },
            'text/xml': {
              schema: {
                name: { type: 'string' },
                image: { type: 'string' },
                address: { type: 'string' }
              }
            }
          }
        }
      }
    }
  }, function (req, reply) {
    reply.send({ id: 1, name: 'Foo', image: 'profile picture', address: 'New Node' })
  })

So instead of having a big switch case where the content-type header sent is sent by the handler, we should let fastify core handle how to serialize it. I mean for this we have the onSerialization lifecycle step.

Also we have to keep in mind, that we use default because we define default as application/json. But using / as content type means default. We dont handle that case.

We should also keep in mind, that we can have weighted values for the Accept-header. So we dont handle them with this PR. This PR expects to have only one value for the accept header.

So what I propose:

  • [ ] implement a ContentTypeSerializer
  • [ ] use fastify-accept-negotiator to handle accept header

Uzlopak avatar Oct 05 '22 08:10 Uzlopak

So what I propose:

@fastify/accepts is designed to support accept header. The user should determine should they support the content-type.

@fastify/accepts-serializer is designed to support custom content-type that is not a json.

I suggest to create a separate issue to track what you needs. This PR is independent feature and should works with @fastify/accepts, @fastify/accepts-serializer and even @fastify/swagger.

climba03003 avatar Oct 05 '22 09:10 climba03003

The unit test would not work out of the box with this implementatiion, if modified like this:

  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'text/html, application/xhtml+xml, application/json;q=0.9' } }, (err, res) => {
    t.error(err)
    t.equal(res.headers['Content-Type'], 'application/json')
    t.equal(res.payload, JSON.stringify({ name: 'Foo', image: 'profile picture', address: 'New Node' }))
    t.equal(res.statusCode, 200)
  })

Maybe we need to integrate @fastify/accepts-serializer into core. Or we need to improve the documentation and give an example on how to do it with @fastify/accepts-serializer

But based on the unit test I would not get a clue that I actually should use @fastify/accepts-serializer to not be forced to bloat the handler.

Uzlopak avatar Oct 05 '22 09:10 Uzlopak

I just want to share how I thought about this, firstly, this shouldn't always be handled the same way as I handled it in the unit test, I just needed to have that big switch case to simplify the test process and test all different cases.

In the end, this feature is checking the content type returned from the handler, it doesn't check the Accept header, so there will be no issue on Fastify side given the modified example above, it's left to the developer how he can benefit from the feature (that if the handler returned a specific content-type, we will validate the response based on that content-type), How the handler is returning different content types? it's left to the developer, he might not even use that switch case, he might have some different business logic that return a content-type based on anything else.

iifawzi avatar Oct 05 '22 09:10 iifawzi

The last commit is addressing the newest APIs that allow the usage of the serializers during the lifetime of a request. Kindly, can you take another look @metcoder95?

iifawzi avatar Oct 05 '22 22:10 iifawzi

I think that implementing this feature in a different way would be a breaking change.

We can target v5 for a follow up PR?

mcollina avatar Oct 14 '22 08:10 mcollina

@climba03003 @Uzlopak PTAL

mcollina avatar Oct 14 '22 08:10 mcollina

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] avatar Oct 15 '23 00:10 github-actions[bot]