Browse Source

Bugfix/eliminate unresolved page title and range not satisfiable error (#7387)

* Eliminate unresolved page title and RangeNotSatisfiableError

* Update changelog
pull/7371/head^2
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
d6295167e0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 11
      CHANGELOG.md
  2. 26
      apps/api/src/app/app.module.ts
  3. 3
      apps/api/src/main.ts
  4. 8
      apps/api/src/middlewares/html-template.middleware.ts
  5. 37
      apps/api/src/middlewares/language-redirect.middleware.ts
  6. 3
      apps/client/project.json
  7. 0
      apps/client/src/assets/index.html
  8. 2
      apps/client/src/index.html

11
CHANGELOG.md

@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Refactored the language redirect of the root path from the static file serving configuration to a dedicated middleware
### Fixed
- Fixed the `RangeNotSatisfiableError` for requests with a `Range` header to the root path caused by the empty `index.html` placeholder
- Fixed the unresolved template literal in the page title while the app is loading from the service worker cache
## 3.30.0 - 2026-07-19 ## 3.30.0 - 2026-07-19
### Added ### Added

26
apps/api/src/app/app.module.ts

@ -14,8 +14,6 @@ import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-g
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { import {
BULL_BOARD_ROUTE, BULL_BOARD_ROUTE,
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES,
THROTTLE_DEFAULT_LIMIT, THROTTLE_DEFAULT_LIMIT,
THROTTLE_DEFAULT_TTL THROTTLE_DEFAULT_TTL
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
@ -143,29 +141,7 @@ import { UserModule } from './user/user.module';
'/api/*wildcard', '/api/*wildcard',
'/sitemap.xml' '/sitemap.xml'
], ],
rootPath: join(__dirname, '..', 'client'), rootPath: join(__dirname, '..', 'client')
serveStaticOptions: {
setHeaders: (res) => {
if (res.req?.path === '/') {
let languageCode = DEFAULT_LANGUAGE_CODE;
try {
const code = res.req.headers['accept-language']
.split(',')[0]
.split('-')[0];
if (
(SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code)
) {
languageCode = code;
}
} catch {}
res.set('Location', `/${languageCode}`);
res.statusCode = StatusCodes.MOVED_PERMANENTLY;
}
}
}
}), }),
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client', '.well-known'), rootPath: join(__dirname, '..', 'client', '.well-known'),

3
apps/api/src/main.ts

@ -1,3 +1,4 @@
import { languageRedirectMiddleware } from '@ghostfolio/api/middlewares/language-redirect.middleware';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { import {
BULL_BOARD_ROUTE, BULL_BOARD_ROUTE,
@ -100,6 +101,8 @@ async function bootstrap() {
}); });
} }
app.use(languageRedirectMiddleware);
const configurationService = app.get(ConfigurationService); const configurationService = app.get(ConfigurationService);
const trustProxy = configurationService.get('TRUST_PROXY'); const trustProxy = configurationService.get('TRUST_PROXY');

8
apps/api/src/middlewares/html-template.middleware.ts

@ -111,7 +111,13 @@ export class HtmlTemplateMiddleware implements NestMiddleware {
); );
try { try {
map[languageCode] = readFileSync(indexHtmlPath, 'utf8'); // Restore the interpolation token which the template replaces with a
// static fallback title to avoid showing an unresolved template
// literal when served without interpolation (e.g. by the service worker)
map[languageCode] = readFileSync(indexHtmlPath, 'utf8').replace(
/<title>.*?<\/title>/,
'<title>${title}</title>'
);
} catch { } catch {
this.logger.warn( this.logger.warn(
`Skipping language '${languageCode}': ${indexHtmlPath} not found` `Skipping language '${languageCode}': ${indexHtmlPath} not found`

37
apps/api/src/middlewares/language-redirect.middleware.ts

@ -0,0 +1,37 @@
import { environment } from '@ghostfolio/api/environments/environment';
import {
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { NextFunction, Request, Response } from 'express';
import { StatusCodes } from 'http-status-codes';
export function languageRedirectMiddleware(
request: Request,
response: Response,
next: NextFunction
) {
if (
!environment.production ||
request.path !== '/' ||
!['GET', 'HEAD'].includes(request.method)
) {
return next();
}
let languageCode = DEFAULT_LANGUAGE_CODE;
try {
const code = request.headers['accept-language'].split(',')[0].split('-')[0];
if ((SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code)) {
languageCode = code;
}
} catch {}
return response.redirect(
StatusCodes.MOVED_PERMANENTLY,
`/${languageCode}${request.url.slice(1)}`
);
}

3
apps/client/project.json

@ -203,9 +203,6 @@
{ {
"command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client" "command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client"
}, },
{
"command": "shx cp apps/client/src/assets/index.html dist/apps/client"
},
{ {
"command": "shx cp apps/client/src/assets/robots.txt dist/apps/client" "command": "shx cp apps/client/src/assets/robots.txt dist/apps/client"
}, },

0
apps/client/src/assets/index.html

2
apps/client/src/index.html

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<html class="h-100 position-relative" lang="${languageCode}"> <html class="h-100 position-relative" lang="${languageCode}">
<head> <head>
<title>${title}</title> <title>Ghostfolio</title>
<base href="/" /> <base href="/" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta content="yes" name="apple-mobile-web-app-capable" /> <meta content="yes" name="apple-mobile-web-app-capable" />

Loading…
Cancel
Save