From 7759065dcca104e2e76775fc802863a7a2ebf668 Mon Sep 17 00:00:00 2001 From: Kyle Ouellette Date: Sun, 1 Mar 2026 15:37:25 -0500 Subject: [PATCH 1/6] fix: support for IPv6 address literals in HOST env var When pages would build URLs for API requests, process.env.HOST would be directly concatenated with process.env.PORT (e.g., `0.0.0.0:5055`). This works for IPv4 address literals but does not work for IPv6 address literals because `:::5055` for example is invalid. This commit adds a check if process.env.HOST contains a colon (:) and if so, wraps the host in square brackets. E.g., `[::]:5055`, which is valid. This allows for users to set the HOST environment variable to IPv6 address literals. fix #2612 --- src/pages/_app.tsx | 9 +++------ src/pages/collection/[collectionId]/index.tsx | 5 ++--- src/pages/movie/[movieId]/index.tsx | 5 ++--- src/pages/tv/[tvId]/index.tsx | 5 ++--- src/utils/urlHelper.ts | 10 ++++++++++ 5 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 src/utils/urlHelper.ts diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 833ecad66b..4159fa5268 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -11,6 +11,7 @@ import type { User } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser'; import '@app/styles/globals.css'; import { polyfillIntl } from '@app/utils/polyfillIntl'; +import { getHostAndPort } from '@app/utils/urlHelper'; import '@fontsource-variable/inter'; import { MediaServerType } from '@server/constants/server'; import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces'; @@ -266,9 +267,7 @@ CoreApp.getInitialProps = async (initialProps) => { if (ctx.res) { // Check if app is initialized and redirect if necessary const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/settings/public` + `http://${getHostAndPort()}/api/v1/settings/public` ); currentSettings = response.data; @@ -286,9 +285,7 @@ CoreApp.getInitialProps = async (initialProps) => { try { // Attempt to get the user by running a request to the local api const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/auth/me`, + `http://${getHostAndPort()}/api/v1/auth/me`, { headers: ctx.req && ctx.req.headers.cookie diff --git a/src/pages/collection/[collectionId]/index.tsx b/src/pages/collection/[collectionId]/index.tsx index e555e41e59..ff8bbbd898 100644 --- a/src/pages/collection/[collectionId]/index.tsx +++ b/src/pages/collection/[collectionId]/index.tsx @@ -1,4 +1,5 @@ import CollectionDetails from '@app/components/CollectionDetails'; +import { getHostAndPort } from '@app/utils/urlHelper'; import type { Collection } from '@server/models/Collection'; import axios from 'axios'; import type { GetServerSideProps, NextPage } from 'next'; @@ -15,9 +16,7 @@ export const getServerSideProps: GetServerSideProps< CollectionPageProps > = async (ctx) => { const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/collection/${ctx.query.collectionId}`, + `http://${getHostAndPort()}/api/v1/collection/${ctx.query.collectionId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } diff --git a/src/pages/movie/[movieId]/index.tsx b/src/pages/movie/[movieId]/index.tsx index d9788a2023..24fd3a826f 100644 --- a/src/pages/movie/[movieId]/index.tsx +++ b/src/pages/movie/[movieId]/index.tsx @@ -1,4 +1,5 @@ import MovieDetails from '@app/components/MovieDetails'; +import { getHostAndPort } from '@app/utils/urlHelper'; import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import axios from 'axios'; import type { GetServerSideProps, NextPage } from 'next'; @@ -15,9 +16,7 @@ export const getServerSideProps: GetServerSideProps = async ( ctx ) => { const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/movie/${ctx.query.movieId}`, + `http://${getHostAndPort()}/api/v1/movie/${ctx.query.movieId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } diff --git a/src/pages/tv/[tvId]/index.tsx b/src/pages/tv/[tvId]/index.tsx index 9659d82ca8..599a7985e0 100644 --- a/src/pages/tv/[tvId]/index.tsx +++ b/src/pages/tv/[tvId]/index.tsx @@ -1,4 +1,5 @@ import TvDetails from '@app/components/TvDetails'; +import { getHostAndPort } from '@app/utils/urlHelper'; import type { TvDetails as TvDetailsType } from '@server/models/Tv'; import axios from 'axios'; import type { GetServerSideProps, NextPage } from 'next'; @@ -15,9 +16,7 @@ export const getServerSideProps: GetServerSideProps = async ( ctx ) => { const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/tv/${ctx.query.tvId}`, + `http://${getHostAndPort()}/api/v1/tv/${ctx.query.tvId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } diff --git a/src/utils/urlHelper.ts b/src/utils/urlHelper.ts new file mode 100644 index 0000000000..669060b85c --- /dev/null +++ b/src/utils/urlHelper.ts @@ -0,0 +1,10 @@ +export function getHostAndPort(): string { + let host = process.env.HOST || 'localhost'; + if (host.includes(':')) { + // If host includes a colon, it's an IPv6 literal and needs to be placed in square brackets + host = `[${host}]`; + } + + const port = process.env.PORT || 5055; + return `${host}:${port}`; +} From d6a66b20c22b341c3b782c66d16f797611127d92 Mon Sep 17 00:00:00 2001 From: Kyle Ouellette Date: Mon, 2 Mar 2026 19:55:17 -0500 Subject: [PATCH 2/6] fix: apply copilot suggestion for port handling --- src/utils/urlHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/urlHelper.ts b/src/utils/urlHelper.ts index 669060b85c..9f9a37fd38 100644 --- a/src/utils/urlHelper.ts +++ b/src/utils/urlHelper.ts @@ -5,6 +5,6 @@ export function getHostAndPort(): string { host = `[${host}]`; } - const port = process.env.PORT || 5055; + const port = Number(process.env.PORT) || 5055; return `${host}:${port}`; } From 25929bcc671e06bd0a6527116331c05d85d343a2 Mon Sep 17 00:00:00 2001 From: Kyle Ouellette Date: Thu, 5 Mar 2026 20:12:30 -0500 Subject: [PATCH 3/6] fix: catch and log express.listen errors --- server/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server/index.ts b/server/index.ts index 37b41fd60d..5b82c22cf2 100644 --- a/server/index.ts +++ b/server/index.ts @@ -267,19 +267,24 @@ app const port = Number(process.env.PORT) || 5055; const host = process.env.HOST; + let httpServer; if (host) { - server.listen(port, host, () => { + httpServer = server.listen(port, host, () => { logger.info(`Server ready on ${host} port ${port}`, { label: 'Server', }); }); } else { - server.listen(port, () => { + httpServer = server.listen(port, () => { logger.info(`Server ready on port ${port}`, { label: 'Server', }); }); } + httpServer.on('error', (err) => { + logger.error('Listen error:', err); + process.exit(1); + }); }) .catch((err) => { logger.error(err.stack); From 7ceb9242048606da51d2b68a5612fa72ab4cc533 Mon Sep 17 00:00:00 2001 From: Kyle Ouellette Date: Wed, 15 Jul 2026 19:24:57 -0400 Subject: [PATCH 4/6] Update Logging Accepted @fallenbagel suggestion Co-authored-by: fallenbagel <98979876+fallenbagel@users.noreply.github.com> --- server/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/index.ts b/server/index.ts index 5b82c22cf2..db3aea0d2f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -282,9 +282,11 @@ app }); } httpServer.on('error', (err) => { - logger.error('Listen error:', err); - process.exit(1); - }); + logger.error('Failed to start server', { + label: 'Server', + message: err.message, + }); + process.exit(1); }) .catch((err) => { logger.error(err.stack); From c9c2b22b61fbfe8c1b3138788eee3454e081dbff Mon Sep 17 00:00:00 2001 From: Kyle Ouellette Date: Wed, 15 Jul 2026 19:40:59 -0400 Subject: [PATCH 5/6] fix: use Address6.isValid for checking if host is an ipv6 literal Apply PR suggestion from @M0NsTeRRR to use the Address6.isValid util from the ip-address package. --- package.json | 1 + pnpm-lock.yaml | 9 +++++++++ server/index.ts | 9 +++++---- src/utils/urlHelper.ts | 6 ++++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 9da491159a..0e2a476247 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "mime": "^4.1.0", "nanoid": "^5.1.7", "next": "16.2.6", + "ip-address": "^10.1.0", "node-cache": "5.1.2", "node-gyp": "12.2.0", "node-schedule": "2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 167cc4d377..a5f1a24b93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: humanize-duration: specifier: ^3.33.2 version: 3.33.2 + ip-address: + specifier: ^10.1.0 + version: 10.2.0 js-yaml: specifier: ^4.1.1 version: 4.1.1 @@ -4868,6 +4871,10 @@ packages: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -13283,6 +13290,8 @@ snapshots: ip-address@10.1.0: {} + ip-address@10.2.0: {} + ip-address@9.0.5: dependencies: jsbn: 1.1.0 diff --git a/server/index.ts b/server/index.ts index db3aea0d2f..38d1f1e14b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -283,10 +283,11 @@ app } httpServer.on('error', (err) => { logger.error('Failed to start server', { - label: 'Server', - message: err.message, - }); - process.exit(1); + label: 'Server', + message: err.message, + }); + process.exit(1); + }); }) .catch((err) => { logger.error(err.stack); diff --git a/src/utils/urlHelper.ts b/src/utils/urlHelper.ts index 9f9a37fd38..c79550a905 100644 --- a/src/utils/urlHelper.ts +++ b/src/utils/urlHelper.ts @@ -1,7 +1,9 @@ +import { Address6 } from 'ip-address'; + export function getHostAndPort(): string { let host = process.env.HOST || 'localhost'; - if (host.includes(':')) { - // If host includes a colon, it's an IPv6 literal and needs to be placed in square brackets + if (Address6.isValid(host)) { + // If host is an IPv6 literal it needs to be placed in square brackets host = `[${host}]`; } From a9130c1d1e5d2b97a4dd9c053d549e42f4042408 Mon Sep 17 00:00:00 2001 From: Kyle Ouellette Date: Sat, 18 Jul 2026 17:33:11 -0400 Subject: [PATCH 6/6] build: update ip-address package to 10.2.0 --- package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0e2a476247..8a5e3dcc11 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "mime": "^4.1.0", "nanoid": "^5.1.7", "next": "16.2.6", - "ip-address": "^10.1.0", + "ip-address": "^10.2.0", "node-cache": "5.1.2", "node-gyp": "12.2.0", "node-schedule": "2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5f1a24b93..0f33b8a92a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,7 +111,7 @@ importers: specifier: ^3.33.2 version: 3.33.2 ip-address: - specifier: ^10.1.0 + specifier: ^10.2.0 version: 10.2.0 js-yaml: specifier: ^4.1.1