Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Framework/Backend/http/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ class HttpServer {
httpConfig.tls = !httpConfig.tls ? false : httpConfig.tls;
httpConfig.hostname = !httpConfig.hostname ? 'localhost' : httpConfig.hostname;
this.limit = !httpConfig.limit ? '100kb' : httpConfig.limit;
this.queryParser = !httpConfig.queryParser ? 'simple' : httpConfig.queryParser;
assert(
['simple', 'extended'].includes(this.queryParser),
`Invalid config value: queryParser must be 'simple' or 'extended', received '${this.queryParser}'`,
);

this.app = express();
this.app.set('query parser', this.queryParser);

if (process.env.NODE_ENV === 'production') {
this.app.set('trust proxy', true);
Expand Down
25 changes: 25 additions & 0 deletions Framework/Backend/test/mocha-http.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,31 @@ describe('HTTP constructor checks', () => {
httpServer = new HttpServer(conf, config.jwt);
assert.strictEqual(httpServer.limit, '10Mb', 'Provided limit was not set');
});
it('should successfully default query parser to simple and keep bracket keys flat', async () => {
httpServer = new HttpServer({ ...config.http }, config.jwt);
httpServer.get('/query', (req, res) => res.json(req.query), { public: true });
assert.strictEqual(httpServer.queryParser, 'simple', 'Default query parser was not set');
await request(httpServer)
.get('/api/query?a=1&a=2&filter[name]=test')
.expect(200)
.expect({ a: ['1', '2'], 'filter[name]': 'test' });
});
it('should successfully use extended query parser when provided in configuration', async () => {
httpServer = new HttpServer({ ...config.http, queryParser: 'extended' }, config.jwt);
httpServer.get('/query', (req, res) => res.json(req.query), { public: true });
assert.strictEqual(httpServer.queryParser, 'extended', 'Provided query parser was not set');
await request(httpServer)
.get('/api/query?fields[]=a&fields[]=b&filter[name]=test')
.expect(200)
.expect({ fields: ['a', 'b'], filter: { name: 'test' } });
});
it('should throw an error if provided query parser is not supported', () => {
assert.throws(
() => new HttpServer({ ...config.http, queryParser: 'qs' }, config.jwt),
/queryParser must be 'simple' or 'extended'/,
);
httpServer = { close: () => Promise.resolve() };
});
});

describe('URL parameters extraction checks', () => {
Expand Down
1 change: 1 addition & 0 deletions Framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ npm install --save @aliceo2/web-ui
* [Consul](./docs/guide/consul.md) - Consul service with simple Read queries
* [JIRA](./docs/guide/jira.md) - Create JIRA issues
* [Notification service](./docs/guide/notification.md) - Trigger and receive notification using Kafka cluster
* [Migration to v3](./docs/guide/migration-v3.md) - Upgrade an application from v2 (Express v4) to v3 (Express v5)

### Frontend guide
- [Template engine](./docs/guide/template-engine.md) - MVC using hyperscript and observable model
Expand Down
7 changes: 6 additions & 1 deletion Framework/docs/guide/http-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#### Instance
```js
const {HttpServer} = require('@aliceo2/web-ui');
HttpServer({port: PORT, hostname: HOSTNAME, tls: TLS_ENABLED, portSecure: HTTPS_PORT, key: TLS_KEY, cert: TLS_CERT, autoListen: AUTO_LISTEN}, JWT_CONF, OPENID_CONF);
HttpServer({port: PORT, hostname: HOSTNAME, tls: TLS_ENABLED, portSecure: HTTPS_PORT, key: TLS_KEY, cert: TLS_CERT, autoListen: AUTO_LISTEN, limit: LIMIT, queryParser: QUERY_PARSER}, JWT_CONF, OPENID_CONF);
```
Where:
* `HTTP_CONF` consists of following fields:
Expand All @@ -21,10 +21,15 @@ Where:
* [`TLS_CERT`] - certificate filepath, TLS must be enabled
* [`AUTO_LISTEN`] - flag that enables/disables automatic listening (default: `true`)
* [`LIMIT`] - controls the maximum request body size. Defaults to '100kb'.
* [`QUERY_PARSER`] - how the URL query string is parsed into `req.query`: `'simple'` or `'extended'` (default: `'simple'`)
* `'simple'` - flat key/value pairs, repeated keys become arrays: `?a=1&a=2` -> `{ a: ['1', '2'] }`; bracket keys stay as they are: `?filter[name]=x` -> `{ 'filter[name]': 'x' }`
* `'extended'` - additionally parses nested objects and arrays: `?filter[name]=x&fields[]=a` -> `{ filter: { name: 'x' }, fields: ['a'] }`. Use it only if your API relies on such parameters
* [`JWT_CONF`] - JWT module config, see [JWT module](json-tokens.md)
* [`OPENID_CONF`] - OpenID config, see [OpenID Connect module](openid.md)


The server is built on top of [Express v5](https://expressjs.com/en/guide/migrating-5.html). If you are upgrading from `@aliceo2/web-ui` v2, see the [migration guide to v3](migration-v3.md).

#### Public methods
```js
listen
Expand Down
103 changes: 103 additions & 0 deletions Framework/docs/guide/migration-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Migration guide - v2 to v3 (Express v5)

`@aliceo2/web-ui` v3 upgrades the HTTP server from [Express v4 to Express v5](https://expressjs.com/en/guide/migrating-5.html).
The public API of `HttpServer` (`get`, `post`, `put`, `patch`, `delete`, `addStaticPath`, ...) stays the same, but the `req` and `res` objects passed to your handlers, as well as the accepted route path syntax, follow Express v5 rules.

This guide lists the changes that may affect applications built on top of the framework.

## Why
- **Async errors are handled**: if an `async` handler or middleware throws or returns a rejected promise, the error is forwarded to the framework's error handler (`500` response) instead of leaving the request hanging.
- **Safer route matching**: Express v5 uses `path-to-regexp` v8, which removes the regular expression features that allowed ReDoS attacks ([CVE-2024-45296](https://github.com/advisories/GHSA-9wv6-86v2-598j)).
- **Safer defaults**: the `simple` query parser is used by default, `res.status()` rejects invalid codes and static files ignore dotfiles.
- **Maintained**: Express v5 is the actively supported release line, v4 only receives maintenance and security fixes.

## Requirements
- Node.js `>= 22.x` (unchanged)

## Upgrade steps

### 1. Update the dependency
```json
"@aliceo2/web-ui": "3.0.0"
```

### 2. Update route paths
Express v5 no longer accepts optional parameters with `?`, inline regular expressions or unnamed wildcards.
Invalid paths **throw when the route is registered**, so the application fails at startup with an error such as:
```
PathError [TypeError]: Unexpected ? at index 24: /environment/:id/:source?
```

| v2 (Express v4) | v3 (Express v5) | Notes |
|---|---|---|
| `/environment/:id/:source?` | `/environment/:id{/:source}` | optional segments are wrapped in braces |
| `/configurations/:key(*)` | `/configurations/*key` | `req.params.key` is now an **array** of path segments, use `req.params.key.join('/')` |
| `/files/*` | `/files/*splat` | wildcards must be named |
| `/:id(\\d+)` | `/:id` | inline regular expressions are not supported, validate the parameter in the handler |

The characters `(`, `)`, `[`, `]`, `?`, `+` and `!` are reserved in paths. Escape them with `\\` if they need to be matched literally.

### 3. Do not assign `req.query`
`req.query` is now a getter. Assigning it throws a `TypeError` in strict mode (ES modules, `'use strict'`) and is silently ignored otherwise:
```
TypeError: Cannot set property query of #<IncomingMessage> which has only a getter
```
Store validated or transformed values elsewhere, for example in `res.locals`:
```js
// v2
req.query = await QueryDto.validateAsync(req.query);
// v3
res.locals.query = await QueryDto.validateAsync(req.query);
```

### 4. Check nested query parameters
Express v5 parses query strings with the `simple` parser by default. Repeated keys still become arrays, but bracket keys are no longer turned into objects:

| Query string | `simple` (v3 default) | `extended` (v2 behaviour) |
|---|---|---|
| `?a=1&a=2` | `{ a: ['1', '2'] }` | `{ a: ['1', '2'] }` |
| `?filter[name]=x` | `{ 'filter[name]': 'x' }` | `{ filter: { name: 'x' } }` |
| `?fields[]=a&fields[]=b` | `{ 'fields[]': ['a', 'b'] }` | `{ fields: ['a', 'b'] }` |

If your API relies on nested parameters, either move to flat parameters (recommended) or opt into the previous behaviour with the new `queryParser` option of the [HTTP server](http-server.md):
```js
const http = new HttpServer({ port: 8080, queryParser: 'extended' }, jwtConfig);
```

### 5. Handle a missing request body
`req.body` is `undefined` (previously `{}`) when the request has no body or its content type is not JSON. Code that reads properties of `req.body` or validates it as an object should use a fallback:
```js
const { name } = req.body ?? {};
await BodyDto.validateAsync(req.body ?? {});
```

### 6. Replace removed request and response methods
| Removed (v2) | Replacement (v3) |
|---|---|
| `req.param('name')` | `req.params.name`, `req.query.name` or `req.body.name` |
| `res.json(obj, status)` | `res.status(status).json(obj)` |
| `res.send(status)` | `res.sendStatus(status)` |
| `res.sendfile(path)` | `res.sendFile(path)` |
| `res.redirect('back')` | `res.redirect(req.get('Referrer') \|\| '/')` |
| `req.acceptsCharset`, `req.acceptsEncoding`, `req.acceptsLanguage` | `req.acceptsCharsets`, `req.acceptsEncodings`, `req.acceptsLanguages` |

### 7. Other behaviour changes
- `res.status(code)` only accepts integers between `100` and `999`, other values throw.
- Static paths added with `addStaticPath` ignore dotfiles (e.g. `.env`, `.hidden/`) by default.
- `req.host` now includes the port number.
- Errors thrown or rejected in `async` handlers are forwarded to the framework's error handler, which replies with `500`. Keep explicit `try/catch` blocks only where a different status code is expected.

## Checklist
The commands below help find code that needs to be reviewed, run them from the application folder:
```bash
# Optional parameters, inline regular expressions and wildcards in routes
grep -rnE ":[A-Za-z_]+\?|:[A-Za-z_]+\(|\(\*\)|/\*['\"\`]" lib/
# Assignments to req.query
grep -rnE "req\.query\s*=[^=]" lib/
# Nested query parameters built by the frontend
grep -rnE "[A-Za-z]\[[^]]*\]=" public/
# Reads of req.body that assume an object
grep -rn "req\.body" lib/
# Removed methods
grep -rnE "req\.param\(|res\.sendfile|'back'|acceptsCharset\b|acceptsEncoding\b|acceptsLanguage\b" lib/
```
Loading
Loading