-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(reply headers): rfc, scaffold #416
base: master
Are you sure you want to change the base?
Conversation
packages/core/src/constants.ts
Outdated
@@ -96,3 +96,5 @@ export const PluginsPriority = [ | |||
|
|||
export const PLUGIN_STATUS_OK = 'ok' | |||
export const PLUGIN_STATUS_FAIL = 'fail' | |||
|
|||
export const kReplyHeaders = Symbol('replyHeaders') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const kReplyHeaders = Symbol('replyHeaders') | |
export const kReplyHeaders = Symbol.for('microfleet:replyHeaders') |
import { Router } from '../../router/factory' | ||
import { kReplyHeaders } from '@microfleet/transport-amqp/lib/constants' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets update @microfleet/transport-amqp and add symbol as exports
something like symbols.replyHeaders
.finally(() => { | ||
raw.properties[kReplyHeaders] = Object.fromEntries(serviceRequest.getReplyHeaders()) | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we put this into finally
handle of try/catch/finally ?
#### `.setReplyHeader(title: string, value: number|string|array<string>): void` | ||
Sets reply header to the map. | ||
|
||
Must normalize title. | ||
Must cast numeric value to string. | ||
Must validate title and value. Must throw exception if any of arguments is invalid. | ||
|
||
If normalized `title` is `set-cookie`, it's being appended to previously set `set-cookie` value. | ||
Else it`s value is being overridden: | ||
|
||
```js | ||
function actionHandler(serviceRequest) { | ||
serviceRequest.setReplyHeader('x-rate-limit', 10000) | ||
serviceRequest.setReplyHeader('x-rate-limit', 20000) | ||
|
||
serviceRequest.setReplyHeader('set-cookie', 'state=sg687gjjsdg69847gjsh; Domain=app.com; Secure; HttpOnly') | ||
serviceRequest.setReplyHeader('set-cookie', 'visitor=798; Domain=app.com; Secure; SameSite=Lax') | ||
|
||
serviceRequest.setReplyHeader('x-location', 'lat=58.259624, lng=55.919243') | ||
serviceRequest.setReplyHeader('X-LOCATION', 'lat=64.547589, lng=39.758303') | ||
} | ||
``` | ||
|
||
This will make the reply headers map be like: | ||
``` | ||
Map { | ||
'set-cookie' => [ | ||
'state=sg687gjjsdg69847gjsh; Domain=app.com; Secure; HttpOnly', | ||
'visitor=798; Domain=app.com; Secure; SameSite=Lax' | ||
], | ||
'x-rate-limit' => '20000', | ||
'x-location' => 'lat=64.547589, lng=39.758303', | ||
} | ||
``` | ||
Which will result in two different header lines for HTTP transport. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest not to create exception (set-cookie) in the code of the core package, but to leave it up to the userland side.
Let's extend the interface Service Request Interface
with the AddReplyHeader
method, which will be to form an array of values for the specified header.
The setReplyHeader
method will be to replace the entire header value with the specified one without exception.
@@ -96,3 +96,5 @@ export const PluginsPriority = [ | |||
|
|||
export const PLUGIN_STATUS_OK = 'ok' | |||
export const PLUGIN_STATUS_FAIL = 'fail' | |||
|
|||
export const kReplyHeaders = Symbol.for('microfleet:replyHeaders') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To achieve independence from transport packages, it is better to declare the symbol in the core package, and use Symbol.for (...)
in the other packages as needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To facilitate the tasks of processing the results inside the actions of Microfleet
-based services, it is better to separate the interfaces into request / reply and implement lightweight wrapper-classes for managing the reply structure.
Instances will be created using factories bound to routes by request options. In the request options, you can specify the lean
attribute, according to which the reply data will be returned as is without creation an instance of the wrapper-class.
each of the Transports effortlessly. | ||
|
||
|
||
In order to respect backwards compatibility, we should be able to choose whether the reply will contain **only |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest simpleResponse
options is not good design. Instead of this option I recommend to use an object with omitting undefined/empty values for 'headers' or 'data'
{ data? , headers? }
Proc - No need to setup additional options. More clean call.
Cons - Breaking change for previous versions.
If backward compatibility is required, it should be handled in a special way in code.
To get the data use the workaround below:
const actualData = response && response.data || response
Reply message headers container. Could be set anywhere during the Request Lifecycle with. Should be used to collect and | ||
deliver headers to original reply message. | ||
|
||
### Methods |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ServiceRequest interface became to much more complicated. So many methods was added to manage only one headers field. The ServiceRequest interface should be solid and minimilstic.
I suggest not to extract new ServiceRequestInterface, but to extend the existing one with one field:
// type ReplyHeaders: { [key: string]: string }
responseHeader? : ReplyHeaders
const promise = dispatch(actionName, opts) | ||
const promise = dispatch(actionName, serviceRequest) | ||
.finally(() => { | ||
raw.properties[kReplyHeaders] = Object.fromEntries(serviceRequest.getReplyHeaders()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor it! Use more descriptive method name for these operations.
Or use adapter specific utility functions to setup transport message props based of request.responseHeaders
import _require from '../../../../../utils/require' | ||
import { Router } from '../../../../router/factory' | ||
import { createServiceRequest} from "./service-request-factory"; | ||
|
||
const setReplyHeader = (response: ResponseObject) => (value: ReplyHeaderValue, title: string) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try to use FP here. Something like that:
import { map } from 'lodash/fp'
const setHeaders = (response) => (key, val) => response.header(key, val)
const setResponseHeaders = setHeaders(response)
const setReplyHeaders = items => map((x) => setResponseHeaders(x))
setReplyHeaders(serviceRequest.getReplyHeaders())
const responseData = await dispatch(actionName, serviceRequest) | ||
|
||
response = h.response(responseData) | ||
serviceRequest.getReplyHeaders().forEach(setReplyHeader(response)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See call above
|
||
if (!callback) return | ||
|
||
const response = options && options.simpleResponse === false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to get rid of the simpleResponse
condition and return an object with scheme: { data, headers }
using approach below:
import { isNil } from 'lodash/fp';
const omitNullOrUndefined = omitBy(isNil);
// ...
callback(err, omitNullOrUndefined(response))
const { ActionTransport } = require('./../../../../src'); | ||
|
||
function SuccessSetHeaderAction(serviceRequest) { | ||
serviceRequest.setReplyHeader('x-your-response-header', 'header value'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use the simplest reply headers declaration ?
serviceRequest.replyHeaders = { 'x-your-response-header' : 'header value' }
Most transport protocols natively have headers in messages. While currently every Transport Router Adapter is able to | ||
parse incoming messages including its optional headers, in order to fully support messaging protocols the Service should | ||
provide a way to set, modify and remove response message headers. That's why we need to implement **common reply headers | ||
API**. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW I would prefer responseHeaders
name instead of replyHeaders
that is symmetric to Request
.
|
||
|
||
Developer experience could be improved in two ways: early headers validation and **strict and common validation policy**. | ||
Early validation will help a developer to avoid errors on transport level. Since the limitations may be different, we |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean "early headers validation"?
|
||
|
||
In order to respect backwards compatibility, we should be able to choose whether the reply will contain **only | ||
message body or include headers** as well. This will be resolved with boolean **simpleResponse** option on the Transport |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice shot;
I guess options are more prefferable and clean way to use this service, especially for users who doesn't familliar with this toolkit;
|
||
Must normalize title. | ||
|
||
### `.isValidReplyHeader(title: string, value: string|string[]): boolean` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about meaning of ### but I think it is another one method in ServiceRequest;
Is it a private method? Or user has to use it manually?
I guess we need to discuss about process of headers validation and describe it in RFC; Does it omit header if it is not valid or throw an exception, etc...
I prefer just omit not valid header but it could be implicit;
``` | ||
|
||
When present, options may contain `simpleResponse` setting. | ||
When `simpleResponse` option value is `false`, callback `result` must be resolved with `data` containing reply message, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it has to work vice versa when we talk about backward compatibility. It there is no simpleResponse => then send old format message (just body) and when we got an option => we send new format message;
Probably, we have to change an option name;
Extract headers collection and set it to the response using Hapi Response Toolkit. | ||
|
||
### SocketIO Transport | ||
Expect new optional argument `options` to be passed on `.emit`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we use different properties to save and handle headers depends on transport layer?
I thought that ServiceRequest has to be transport agnostic.
|
||
const setReplyHeader = (response: ResponseObject) => (value: ReplyHeaderValue, title: string) => { | ||
// set-cookie header exceptional case is correctly implemented by hapi | ||
return Array.isArray(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we normalize that and always get an array?
@@ -58,10 +58,10 @@ export default function attachRouter(service: Microfleet, config: any): HapiPlug | |||
server.route({ | |||
method: ['GET', 'POST'], | |||
path: '/{any*}', | |||
async handler(request: Request) { | |||
async handler(request: Request, h: ResponseToolkit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't used hapi. Is it common practise to use letters as arguments of functions? Especially in TS;
const actionName = fromPathToName(request.path, config.prefix) | ||
const handler = hapiRouterAdapter(actionName, service) | ||
return handler(request) | ||
return handler(request, h) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it is prefferable to use one style in code. And I prefer this one instead this:
serviceRequest.getReplyHeaders().forEach(setReplyHeader(response))
And I recommend to use another word to describe variable handler. Probably you can call it hapiHandler. It is unintuitive when function name and variable inside are same;
No description provided.