- add SSE Example with simple chat #329
- fix: secure header assignment to avoid process crash. #330
- avoid stream hanging on error. #337
- fix: request url not logged correctly. #341
- refactor tests. #342
- update index.d.ts #338, #340
- update qs package to mitigate CVE-2022-24999. #320
- correct types for route middleware functions. #327
- incorrect key for calloptions type. #328
- add support for qs options. #326
- improving d.ts
- register on close and error event on response. #299
- resolve promise when close event is fired for res. #301
- add default reqTimeout. #312
- improve rate limiter. #317
- add 201 Created to status codes. #297
- d.ts for v0.10.2 v0.10.3 by @0x0a0d in #280
- skip services without settings by @kechkibet in #283
- Fix to comment in multi-auth example by @machinaexdeo in #286
- Add alias as last parameter by @icebob in #290
- Fix cors problem in assets by @moonrailgun in #289
- Broadcast event after alias regeneration by @icebob in #292
- update dependencies.
- change to @fastify/busboy implementation. #287
Named authenticate & authorize methods #275
You can define custom authentication and authorization methods for every routes. In this case you should set the method name instead of true
value.
module.exports = {
mixins: ApiGatewayService,
settings: {
routes: [
{
path: "/aaa",
authentication: "aaaAuthn",
authorization: "aaaAuthz",
aliases: {
"GET hello": "test.hello"
}
},
{
path: "/bbb",
authentication: "bbbAuthn",
authorization: "bbbAuthz",
aliases: {
"GET hello": "test.hello"
}
},
{
path: "/ccc",
authentication: true,
authorization: true,
aliases: {
"GET hello": "test.hello"
}
}
]
},
methods: {
aaaAuthn() {
// ... do authn
},
aaaAuthz() {
// ... do authz
},
bbbAuthn() {
// ... do authn
},
bbbAuthz() {
// ... do authz
},
authenticate() {
// ... do authn
},
authorize() {
// ... do authz
}
}
}
module.exports = {
name: 'api',
mixins: [ApiGateway],
settings: {
rateLimit: rateLimitGlobalConfig,
routes: [
{
path: '/withLocalRateLimit',
// now you can pass rateLimit here
// it will override the global rateLimit
rateLimit: rateLimitConfig,
aliases: {
'GET /2': 'test.2'
}
},
{
path: '/withGlobalRateLimit',
aliases: {
'GET /1': 'test.1'
}
}
]
}
}
- update dependencies.
- add support for custom cors origin function. #274
- typescript definition file is rewritten. #259
Many developers issued that version 0.10 doesn't support multiple routes with the same path. This version fixes it but you should give a unique name for the routes if they have same path.
Example
You can call /api/hi
without auth, but /api/hello
only with auth.
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
path: "/api",
routes: [
{
name: "no-auth-route", // unique name
path: "/",
aliases: {
hi: "greeter.hello",
}
},
{
name: "with-auth-route", // unique name
path: "/",
aliases: {
"hello": "greeter.hello",
},
authorization: true
}
]
}
};
- add
removeRouteByName(name: string)
method to remove a route by its name.
- set the default JSON bodyparser if
bodyParser: true
. #258 - add
pathToRegexpOptions
to route options to make available to pass options topath-to-regexp
library. #268 - add
debounceTime
to route options to make available to change the debounce time at service changes. #260 - new
errorHandler
method to allow developers to change the default error handling behaviour. #266 - fixes CORS preflight request handling in case of full-path aliases. #269
- update dependencies
Many users issued that at file uploading the response always wrapped into an array even single file uploading. This issues is fixed in this version.
If you define files: 1
in busboy settings, the response won't be wrapped into array.
Example
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
path: "/upload",
routes: [
{
path: "/upload",
busboyConfig: {
limits: {
files: 1
}
},
}
]
}
};
In early version, if you added multiple routes, you should always set JSON body-parser. As of v0.10, it's the new default, if you don't define bodyParsers
in the route options. If you don't want to use any body-parsers just set bodyParsers: false
.
Example: disable body parsers
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
routes: [
{
path: "/api",
bodyParsers: false
}
]
}
};
The addRoute
and removeRoute
methods exposed to service actions, as well. It means, you can add/remove routes from remote nodes, as well.
Adding a new route
broker.call("api.addRoute", {
route: {
path: "/api",
aliases: {
"hi": "greeter.hello"
}
},
toBottom: true // add this route to the end of the route list.
})
Removing a route
broker.call("api.removeRoute", { path: "/api" });
There are two new logging service settings: logRequest
, logResponse
. You can define the logging level for request & response log messages.
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
logRequest: "debug", // Logging with debug level
logResponse: false, // Disable logging
//.....
}
};
There a new rootCallOptions
property in the service settings. Here you can define the root Context
calling options. It can be a static Object
or a Function
. It can be useful to take some data from the req
and put them to the calling options (like tracing informations)
Example with static object
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
routes: [/*...*/],
rootCallOptions: {
timeout: 500
}
}
};
Example with Function which takes tracing information from the req and put them to the calling options
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
routes: [/*...*/],
rootCallOptions(options, req, res) {
if (req.headers["traceparent"]) {
// More info https://www.w3.org/TR/trace-context/#traceparent-header
const traceparent = req.headers["traceparent"].toLowerCase();
if (traceparent.match(/^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/)) {
const [version, id, parentSpan, flags] = traceparent.split("-");
const sampled = (flags & FLAG_SAMPLED) == FLAG_SAMPLED;
options.parentSpan = {
id: parentSpan,
traceID: id,
sampled
};
}
} else {
// Look for X-B3-Traceid, X-B3-Spanid
options.parentSpan = {};
if (req.headers["x-b3-traceid"]) {
options.parentSpan.traceID = req.headers["x-b3-traceid"].toLowerCase();
options.parentSpan.sampled = true;
}
if (req.headers["x-b3-spanid"]) {
options.parentSpan.id = req.headers["x-b3-spanid"].toLowerCase();
}
}
}
}
};
You can define multiple REST aliases in the action definitions.
module.exports = {
name: "posts",
settings: {
rest: ["/posts", "/v1/posts"]
},
actions: {
find: {
rest: ["GET /", "GET /all"]
handler(ctx) {}
}
}
};
Several issues has been fixed with multipart handling.
- URL parameters (e.g.
/api/upload/:tenant/:folder
) is available viactx.meta.$params
- Multipart fields is available via
ctx.meta.$multipart
ctx.params.$params
is not available, usectx.meta.$params
- The target action is called if no uploaded files but has multipart fields. In this case
ctx.params
is{}
and the fields are inctx.meta.$multipart
.
- set response header from
ctx.meta
in case of errors, as well. - update dependencies.
- update index.d.ts.
- remove deprecated
publish: false
condition. Use `visibility: "public", instead.
- remove empty log lines
- add
encodeResponse(req, res, data)
method. You can overwrite it in order to use other response encoding instead of JSON.
Due to Node 6 & 8 LTS end of life, the minimum Node version is 10.
In the previous version the mappingPolicy
default value was all
which means, you can call
any services via API Gateway which accepted by whitelist. This setting is not too secure.
From this version, the default value is restrict
if at least one alias is defined in route options.
If there are not aliases & mappingPolicy
defined, the behaviour will be the old one.
We have removed the middleware
service setting because it was not straightforward. Therefore, we have created a new server
setting.
If server: true
(which is the default value), API Gateway will create a HTTP(s) server. If server: false
, it won't create a HTTP server, so you can use API Gateway as an Express middleware.
Before
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
middleware: true
}
}
After
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
server: false
}
}
sendResponse
signature is changed tothis.sendResponse(req, res, data)
API Gateway has implemented file uploads. You can upload files as a multipart form data (thanks for busboy library) or as a raw request body. In both cases, the file is transferred to an action as a Stream. In multipart form data mode you can upload multiple files, as well.
Please note, you have to disable other body parsers in order to accept files.
Example
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
path: "/upload",
routes: [
{
path: "",
// You should disable body parsers
bodyParsers: {
json: false,
urlencoded: false
},
aliases: {
// File upload from HTML multipart form
"POST /": "multipart:file.save",
// File upload from AJAX or cURL
"PUT /": "stream:file.save",
// File upload from HTML form and overwrite busboy config
"POST /multi": {
type: "multipart",
// Action level busboy config
busboyConfig: {
limits: {
files: 3
}
},
action: "file.save"
}
},
// Route level busboy config.
// More info: https://github.com/mscdex/busboy#busboy-methods
busboyConfig: {
limits: {
files: 1
}
// Can be defined limit event handlers
// `onPartsLimit`, `onFilesLimit` or `onFieldsLimit`
},
mappingPolicy: "restrict"
}
]
}
});
HTTP2 experimental server has been implemented into API Gateway. You can turn it on with http2: true
service setting.
Example
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
port: 8443,
// HTTPS server with certificate
https: {
key: fs.readFileSync("key.pem"),
cert: fs.readFileSync("cert.pem")
},
// Use HTTP2 server
http2: true
}
});
The this.addRoute(opts, toBottom = true)
new service method is added to add/replace routes. You can call it from your mixins to define new routes (e.g. swagger route, graphql route...etc).
The function detects that the route is defined early. In this case, it will replace the previous route configuration with the new one.
To remove a route, use the this.removeRoute("/admin")
method. It removes the route by path.
Thank to tiaod for ETag implementation. PR: #92
Example
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
// Service-level option
etag: false,
routes: [
{
path: "/",
// Route-level option.
etag: true
}
]
}
}
The etag
option value can be false
, true
, weak
, strong
, or a custom Function.
Custom ETag generator function
module.exports = {
mixins: [ApiGateway],
settings: {
// Service-level option
etag: (body) => generateHash(body)
}
}
Please note, it doesn't work with stream responses. In this case, you should generate the etag by yourself.
Example
module.exports = {
name: "export",
actions: {
// Download response as a file in the browser
downloadCSV(ctx) {
ctx.meta.$responseType = "text/csv";
ctx.meta.$responseHeaders = {
"Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`,
"ETag": '<your etag here>'
};
return csvFileStream;
}
}
}
The auto-aliasing means you don't have to add all service aliases to the routes, the Gateway can generate it from service schema. If a new service is entered or leaved, Gateway regenerate aliases.
To configure which services are used in route use the whitelist.
Example
// api.service.js
module.exports = {
mixins: [ApiGateway],
settings: {
routes: [
{
path: "/api",
whitelist: [
"posts.*",
"test.*"
],
aliases: {
"GET /hi": "test.hello"
},
autoAliases: true
}
]
}
};
// posts.service.js
module.exports = {
name: "posts",
version: 2,
settings: {
// Base path
rest: "posts/"
},
actions: {
list: {
// Expose as "/api/v2/posts/"
rest: "GET /",
handler(ctx) {}
},
get: {
// Expose as "/api/v2/posts/:id"
rest: "GET /:id",
handler(ctx) {}
},
create: {
rest: "POST /",
handler(ctx) {}
},
update: {
rest: "PUT /:id",
handler(ctx) {}
},
remove: {
rest: "DELETE /:id",
handler(ctx) {}
}
}
};
The generated aliases
GET /api/hi => test.hello
GET /api/v2/posts => v2.posts.list
GET /api/v2/posts/:id => v2.posts.get
POST /api/v2/posts => v2.posts.create
PUT /api/v2/posts/:id => v2.posts.update
DELETE /api/v2/posts/:id => v2.posts.remove
Example to define full path alias
// posts.service.js
module.exports = {
name: "posts",
version: 2,
settings: {
// Base path
rest: "posts/"
},
actions: {
tags: {
// Expose as "/tags/" instead of "/api/v2/posts/tags"
rest: {
method: "GET",
fullPath: "/tags"
},
handler(ctx) {}
}
}
};
- new
optimizeOrder: true
setting in order to optimize route & alias paths (deeper first). Default:true
. - new
logging
route option to disable request logging. It can be useful for health check routes. Default:true
. - tilde (
~
) replace issue fixed. #98 - throw
503
-ServiceUnavailableError
when a service defined in aliases but not available. Ref: #27 - new
internalServiceSpecialChar
service setting to override special char for internal services (~
) - new
httpServerTimeout
setting to overwrite the default HTTP server timeout. #126 - add
reformatError
method to change the response error object (remove or add fields, localize error message...etc). - new
listAliases
action to get all registered route aliases. - remove bluebird dependency & using native Promise & async/await.
- allow multiple whitespaces between method & path in aliases.
- fix
req.url
, addreq.originalUrl
andreq.baseUrl
for better middleware support (e.g. support static serving in subpath). - update deps
- use
Promise
instarted
&stopped
handlers. - disable 4xx errors with
log4XXResponses
setting.
This authenticate
method is similar to authorize
. You have access to req
, res
and route
objects and you can authenticate the user from the request.
The returned data is saved to the ctx.meta.user
. To enable this logic set authentication: true
in route options.
Example
module.exports = {
name: "api",
mixins: [ApiGatewayService],
settings: {
routes: [
{
// Enable authentication
authentication: true
}
]
},
methods: {
authenticate(ctx, route, req, res) {
let accessToken = req.query["access_token"];
if (accessToken) {
if (accessToken === "12345") {
return Promise.resolve({ id: 1, username: "john.doe", name: "John Doe" });
} else {
return Promise.reject();
}
} else {
return Promise.resolve(null);
}
}
}
});
- update dependencies.
- added
.npmignore
- fix missing dependency.
- fix middleware array promise-chaining bug
- handle terminated requests in middlewares
- update webpack-vue example to be up-to-date.
In previous versions of Moleculer Web, you couldn't manipulate the data
in onAfterCall
. Now you can, but you must always return the new or original data
.
Modify only headers
broker.createService(ApiGatewayService, {
settings: {
routes: [{
onAfterCall(ctx, route, req, res, data) {
res.setHeader("X-Custom-Header", "123456");
// Must return the original `data`
return data;
}
}]
}
});
Modify (wrap) the original data
broker.createService(ApiGatewayService, {
settings: {
routes: [{
onAfterCall(ctx, route, req, res, data) {
// Wrap the original data to a new object
return {
other: "things",
data: data
};
}
}]
}
});
The onBeforeCall
and authorize
hooks are called before custom alias functions too.
And you have access to Context as req.$ctx
or res.$ctx
In early versions the *
match string is enabled to call all services & actions. The matcher changed, in new versions use the **
(double star) match string for the same function.
Since Moleculer v0.12, you can use ctx.meta
to send back response headers to the Moleculer Web.
The old method is deprecated but works.
Available meta fields:
ctx.meta.$statusCode
- setres.statusCode
.ctx.meta.$statusMessage
- setres.statusMessage
.ctx.meta.$responseType
- setContent-Type
in header.ctx.meta.$responseHeaders
- set all keys in header.ctx.meta.$location
- setLocation
key in header for redirects.
Old method
module.exports = {
name: "export",
actions: {
downloadCSV:
responseType: "text/csv",
responseHeaders: {
"Content-Disposition": "attachment; filename=\"data.csv\"",
},
handler() {
return "...";
}
}
}
}
New method
module.exports = {
name: "export",
actions: {
// Download a file in the browser
downloadCSV(ctx) {
ctx.meta.$responseType = "text/csv";
ctx.meta.$responseHeaders = {
"Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`
};
return "...";
}
// Redirect the request
redirectSample(ctx) {
ctx.meta.$statusCode = 302;
ctx.meta.$location = "/test/hello";
}
}
}
Thanks for @hwuethrich, Moleculer Web supports arrays & nested objects in querystring.
GET /api/opt-test?a=1&a=2
a: ["1", "2"]
GET /api/opt-test?foo[bar]=a&foo[bar]=b&foo[baz]=c
foo: {
bar: ["a", "b"],
baz: "c"
}
There is support to use error-handler middlewares in the API Gateway. So if you pass an Error
to the next(err)
function, it will call error handler middlewares which have signature as (err, req, res, next)
.
broker.createService({
mixins: [ApiService],
settings: {
// Global middlewares. Applied to all routes.
use: [
cookieParser(),
helmet()
],
routes: [
{
path: "/",
// Route-level middlewares.
use: [
compression(),
passport.initialize(),
passport.session(),
function(err, req, res, next) {
this.logger.error("Error is occured in middlewares!");
this.sendError(req, res, err);
}
],
preValidate
has been removed.- fix multiple CORS origin handling. Thanks for @felipegcampos
- if
X-Correlation-Id
is in the request header, it is used asrequestID
inContext
. - types in errors have been changed (removed
ERR_
prefix) path-to-regexp
is updated to v2.x.x
- update dependencies.
- fix Bluebird cancellation error
UnhandledPromiseRejectionWarning: Error: cannot enable cancellation after promises are in use
#202 -update dependencies
- turnable pre-validation with
preValidate
setting. Default totrue
in order to backward compatibility.broker.createService({ mixins: [ApiService], settings: { // Disable pre-validation at action calls preValidate: false } })
- fix CORS
OPTIONS
handling. #30
The route
first argument is removed. The new signature of function is function(req, res) {}
. To access to route use the req.$route
property.
However you can use an array of Function
for aliases. With it you can call middlewares. In this case the third argument is next
. I.e.: function(req, res, next) {}
.
- better error handling. Always returns with JSON error response.
- The charset is
UTF-8
forapplication/json
responses. logRequestParams
setting to log the request parameters. Use log level value i.e."debug"
,"info"
ornull
to disable.logResponseData
setting to log the response data. Use log level value i.e."debug"
,"info"
ornull
to disable.req.$service
&res.$service
is pointed to the service instance.req.$route
&res.$route
is pointed to the route definition.req.$params
is pointed to the resolved parameters (from query string & post body)req.$alias
is pointed to the alias definition.req.$endpoint
is pointed to the resolved action endpoint. It containsaction
andnodeID
.
Support middlewares in global, routes & aliases.
broker.createService({
mixins: [ApiService],
settings: {
// Global middlewares. Applied to all routes.
use: [
cookieParser(),
helmet()
],
routes: [
{
path: "/",
// Route-level middlewares.
use: [
compression(),
passport.initialize(),
passport.session(),
serveStatic(path.join(__dirname, "public"))
],
aliases: {
"GET /secret": [
// Alias-level middlewares.
auth.isAuthenticated(),
auth.hasRole("admin"),
"top.secret" // Call the `top.secret` action
]
}
}
]
}
});
It supports custom response headers to define in action definition.
module.exports = {
name: "export",
actions: {
downloadCSV:
responseHeaders: {
"Content-Disposition": "attachment; filename=\"data.csv\"",
"Content-Type": "text/csv"
},
handler() {
return "...";
}
}
}
}
You can add route & global custom error handlers.
broker.createService({
mixins: [ApiService],
settings: {
routes: [{
path: "/api",
// Route error handler
onError(req, res, err) {
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.writeHead(500);
res.end(JSON.stringify(err));
}
}],
// Global error handler
onError(req, res, err) {
res.setHeader("Content-Type", "text/plain");
res.writeHead(501);
res.end("Global error: " + err.message);
}
}
}
- Webpack - webpack-dev-middleware example
- Webpack & Vue - Webpack, VueJS, HMR example
- add
mappingPolicy
route option
- add CORS headers
- add Rate limiter
- compatibility with Moleculer >= v0.11.x
- update Moleculer to v0.10
module.exports = {
name: "test",
actions: {
dangerZone: {
publish: false,
handler(ctx) {
return "You cannot call this action via API Gateway!";
}
}
}
};
The route
has a callOptions
property which is passed to broker.call
. So you can set timeout
, retryCount
or fallbackResponse
options for routes.
broker.createService(ApiGatewayService, {
settings: {
routes: [{
callOptions: {
timeout: 1000, // 1 sec
retryCount: 0,
//fallbackResponse: { ... },
// or
//fallbackResponse(ctx, err) { ... }
}
}]
}
});
- in the REST shorthand, the
GET /
calls thelist
action instead offind
. The reason islist
action inmoleculer-db
is support pagination
- changed order of param handling
ctx.params = Object.assign({}, body, query, params)
. - moved
onBeforeCall
beforeauthorize
in request flow. So you can also reach unauthorized requests inonBeforeCall
handler. - the
sendResponse
method has new arguments:sendResponse(ctx, route, req, res, data, responseType)
There is available to use custom function in aliases. In this case you got req
& res
and you should return with the response. Use it for example file uploads. You can find example in the full example.
Usage
...
aliases: {
"add/:a/:b": "math.add",
"GET sub": "math.sub",
"POST upload"(route, req, res) {
//Do something and call res.end()
}
}
...
There is a new camelCaseNames
option in route setting. If it is true, the service will convert the received action name to camelCase name.
Usage
broker.createService(ApiGatewayService, {
settings: {
routes: [{
camelCaseNames: true
}]
}
});
broker.createService({
name: "test",
actions: {
sayHi(ctx) {
return "Hi!"
}
}
});
// Start server
broker.start();
In the above example the sayHi
action can be called with http://localhost:3000/test/say-hi as well.
Available errors:
Class | Params | Description |
---|---|---|
UnAuthorizedError |
type , data |
Unauthorized HTTP error (401) |
ForbiddenError |
type , data |
Forbidden HTTP error (403) |
BadRequestError |
type , data |
Bad Request HTTP error (400) |
Type contants:
ERR_NO_TOKEN
ERR_INVALID_TOKEN
ERR_UNABLE_DECODE_PARAM
Usage
const { UnAuthorizedError, ERR_NO_TOKEN } = require("moleculer-web").Errors;
...
actions: {
update(ctx) {
if(!ctx.meta.user)
return Promise.reject(new UnAuthorizedError(ERR_NO_TOKEN));
}
}
...
It is possible to use RESTful aliases which routed to CRUD service actions.
Usage
broker.createService(ApiGatewayService, {
settings: {
routes: [{
// RESTful aliases
aliases: {
"REST posts": "posts"
}
}]
}
});
// Start server
broker.start();
The "REST posts": "posts"
will be extracted to these aliases:
"GET posts": "posts.find",
"GET posts/:id": "posts.get",
"POST posts": "posts.create",
"PUT posts/:id": "posts.update",
"DELETE posts/:id": "posts.remove"
Example: examples/rest
It is possible to use named parameters in aliases. Named paramters are defined by prefixing a colon to the parameter name (:name
)
Usage
broker.createService(ApiGatewayService, {
settings: {
routes: [{
path: "/api",
aliases: {
"GET greeter/:name": "test.greeter",
"optinal-param/:name?": "test.echo",
"repeat-param/:args*": "test.echo",
"GET /": "test.hello"
}
}]
}
});
// Start server
broker.start();
Example: examples/full
The route of service has onBeforeCall
and onAfterCall
hooks. It can be asynchronous if return with Promise. In methods the this
is pointed to Service instance. So you can access the service methods & broker.
Usage
broker.createService(ApiGatewayService, {
settings: {
routes: [{
// Call before `broker.call`
onBeforeCall(ctx, route, req, res) {
// Save request headers to context meta
ctx.meta.userAgent = req.headers["user-agent"];
},
// Call after `broker.call` and before send back the response
onAfterCall(ctx, route, req, res, data) {
res.setHeader("X-Custom-Header", "123456");
}
}]
}
});
// Start server
broker.start();
Example: examples/full
You can use Moleculer-Web as a middleware for ExpressJS.
Usage
const svc = broker.createService(ApiGatewayService, {
settings: {
middleware: true
}
});
// Create Express application
const app = express();
// Use ApiGateway as middleware
app.use("/api", svc.express());
// Listening
app.listen(3000);
// Start server
broker.start();
Example: examples/express
For more information check the full or authorization examples or readme
First release.