At the moment, the return value of a controller method/router handler is converted to the response using the following hard-coded algorithm:
response.write() as-is and no content-type is set.We need a more flexible setup, we should at least honor "produces" configuration from the API specification.
A controller method should be able to specify media type for the responses object with @operation. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#response-object
_Note by @bajtos: I believe this is already supported, see e.g. the TodoController in our examples: https://github.com/strongloop/loopback-next/blob/cb308add60bbe938c8e85812676eb817e9e0efc9/examples/todo/src/controllers/todo.controller.ts#L32_
A controller method should be able to return JS representation of the response body or a wrapper corresponding to the response objects. The goal: allow controller methods to control response status code and headers. E.g. Location for 201 Created responses to POST requests creating new model instances, Content-Type and Content-Disposition for file downloads.
LoopBack should be able to match the response media types to the Accept request header to determine what content type to be produced for a given request. It will set Content-Type response header.
Have built in support for application/json and text/plain media-types.
EDIT: Move to another task
- LoopBack runtime should define an extension point to register http response serializers that can be used to serialize the JS plain/wrapper object into a http response. A serializer should be able to handle certain media types, for example, json, xml, or protocol buffer. The serializer just has to deal with the http response object as a canonical representation.
We probably should start to refactor https://github.com/strongloop/loopback-next/tree/master/packages/repository/src/types into its own package to form the LB.next typing system and leverage it for parameter parsing/conversion/serialization.
Labelling as non-MVP, because we have a simple workaround available - users can use their own implementation of send sequence action.
@bajtos can you give an example of that? I'm currently fighting the content-type header (it really needs to send application/json when we return a single string), and using the sugar for @get(path, spec), but it seems to ignore whatever is put into the "produces" section of the spec object.
@dustinbarnes Take a look at the default implementation of send() here and here.
Here is what you need to do:
writeResultToResponse function.@bajtos @dustinbarnes
Registering your custom send action involves injecting on the RestBindings.SequenceActions.SEND key with a custom Provider for Send.
application.ts
export class YourApp extends RepositoryMixin(Application) {
constructor() {
super();
// Assume your controller setup and other items are in here as well.
this.bind(RestBindings.SequenceActions.SEND).toProvider(CustomSendProvider);
}
custom-send-provider.ts
import { Send } from "@loopback/rest";
import { Provider, BoundValue } from "@loopback/context";
import { writeResultToResponse } from "@loopback/rest";
export class CustomSendProvider implements Provider<BoundValue>{
value(): Send | Promise<Send> {
return writeResultToResponse; // Replace this with your own implementation.
}
}
If you're implementing _multiple servers_ in your application, binding this key at the Application-level context will make that binding the default for all of the servers connected to it. This means that if you wanted different custom providers for different RestServer instances, you'd want to perform those bindings elsewhere (like in an async override of the start method).
In this example, PrivateSendProvider might allow stack traces to be returned via the response, whereas PublicSendProvider will not.
in application.ts
async start() {
const publicServer = await this.getServer('public');
const privateServer = await this.getServer('private');
publicServer.bind(RestBindings.SequenceActions.SEND).toProvider(PublicSendProvider);
privateServer.bind(RestBindings.SequenceActions.SEND).toProvider(PrivateSendProvider);
await super.start(); // Don't forget to call start!
}
I created a follow-up issue to capture the instructions from Kevin in our docs - see https://github.com/strongloop/loopback-next/issues/863.
Does it fall under the "Validation and type conversion" epic #755 ?
I don't think so. As I understand #755, it deals with input parameters. This story deals with outputs.
I think the best epic to assign this story to is an epic for implementing a local in-process API Explorer and/or a capability to server static files.
@bajtos Could we get an acceptance criteria for this issue? Thanks
@bajtos ^^^
Acceptance criteria:
media type for the responses object with @operation. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#response-objectdescription: A complex object array response
content:
application/json:
schema:
type:
$ref: '#/components/schemas/Customer'
export class CustomerController {
@get('/:id', {responses: {...}})
async findById(id: string): Customer {
return await repo.findById(id);
}
}
or
export class CustomerController {
@get('/:id', {responses: {...}})
async findById(id: string): RestResponse {
return {
statusCode: 200,
headers: {...};
content: {
'application/json': customer,
},
links: {...};
}
}
}
LoopBack should be able to match the response media types to the Accept request header to determine what content type to be produced for a given request. It will set Content-Type response header.
LoopBack runtime should define an extension point to register http response serializers that can be used to serialize the JS plain/wrapper object into a http response. A serializer should be able to handle certain media types, for example, json, xml, or protocol buffer. For example:
serialize(ctx: RequestContext, contentType: string, responseObject) {
// ...
}
Great acceptance criteria @raymondfeng. Just a discussion point ... I think we need to provide a way to provide some defaults for some formats if possible so the user doesn't need to define these for every operation. I'm thinking the defaults would be text/html / application/json ... Maybe octet-stream / xml as well?
Or perhaps the extension system you describe can generate the response in the format requested and if the header isn't set then we default to application/json and/or provide the user an option to set an Application wide response format to use unless otherwise requested.
Just a discussion point ... I think we need to provide a way to provide some defaults for some formats if possible so the user doesn't need to define these for every operation.
馃憤 +1
According to #1449, futher REST layer improvements #1452 are out of scope of 4.0 GA.
Hello. I am interested in having a JSONP response. I seem to believe this was available in LB3 out of the box. @bajtos when you refer to:
A controller method should be able to return JS representation of the response body or a wrapper corresponding to the response objects.
I assume you mean JSONP? If not, what would be the best way to achieve JSONP.
There seems to be no way of differentiating the response based on the "Accept" header being sent.
@acrodrig
A controller method should be able to return JS representation of the response body or a wrapper corresponding to the response objects.
I assume you mean JSONP?
Not really. We want to allow the controller method to handle more aspects of the HTTP response that will be produced, e.g. status code, headers, etc.
See https://github.com/strongloop/loopback-next/issues/436#issuecomment-388879072
export class CustomerController {
@get('/:id', {responses: {...}})
async findById(id: string): RestResponse {
return {
statusCode: 200,
headers: {...};
content: {
'application/json': customer,
},
links: {...};
}
}
}
If not, what would be the best way to achieve JSONP.
Our current recommendation is to write a custom implementation of sequence action send, see https://loopback.io/doc/en/lb4/Sequence.html#customizing-sequence-actions.
I seem to believe this was available in LB3 out of the box.
Yes, indeed - see https://github.com/strongloop/strong-remoting/blob/33fbd72fb46035f707c3c62dce6b36ab075fb61e/lib/http-context.js#L480-L483
I created a new issue to keep track of this feature - see https://github.com/strongloop/loopback-next/issues/2752
What about this, I think it can work
export class CustomerController {
@get('/:id', {responses: {...}})
async findById(id: string, @inject(Http.RESPONSE) response): Response {
return Object.assign(response, {
statusCode: 200,
headers: {...};
content: {
'application/json': customer,
}
links: {...};
})
}
}
because writeResultToResponse bypass response if controller return the response
Most helpful comment
@bajtos @dustinbarnes
Registering your custom send action involves injecting on the
RestBindings.SequenceActions.SENDkey with a custom Provider for Send.application.ts
custom-send-provider.ts
If you're implementing _multiple servers_ in your application, binding this key at the Application-level context will make that binding the default for all of the servers connected to it. This means that if you wanted different custom providers for different RestServer instances, you'd want to perform those bindings elsewhere (like in an async override of the
startmethod).In this example, PrivateSendProvider might allow stack traces to be returned via the response, whereas PublicSendProvider will not.
in application.ts