Skip to content
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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft

Conversation

bludzhd
Copy link
Contributor

@bludzhd bludzhd commented Apr 21, 2020

No description provided.

@@ -96,3 +96,5 @@ export const PluginsPriority = [

export const PLUGIN_STATUS_OK = 'ok'
export const PLUGIN_STATUS_FAIL = 'fail'

export const kReplyHeaders = Symbol('replyHeaders')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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'
Copy link
Member

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

Comment on lines +50 to +52
.finally(() => {
raw.properties[kReplyHeaders] = Object.fromEntries(serviceRequest.getReplyHeaders())
});
Copy link
Member

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 ?

Comment on lines +122 to +157
#### `.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.

Copy link

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')
Copy link

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.

Copy link

@triadium triadium left a 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
Copy link

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
Copy link

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())
Copy link

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) => {
Copy link

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))
Copy link

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
Copy link

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');
Copy link

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**.
Copy link

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

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

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`

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,

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`:

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)

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) {

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)

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;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants