Skip to content
Draft
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
37 changes: 37 additions & 0 deletions .changes/019-server-bind-host-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
group: platform
component: Framework
impact: breaking
category: Breaking changes
---

## Summary

You can now choose which network address and port your web server listens on. Add `Application.withHost "127.0.0.1"` to accept connections from the same machine only, and `Application.withPort 9090` to change the port. `host` also accepts `"*"` (all addresses, IPv4 and IPv6), `"*4"`, `"*6"` or a literal address.

Loopback (`"127.0.0.1"`) is the right choice when your reverse proxy runs on the same machine or in the same container as the app. If the proxy runs somewhere else (its own container with its own network, another host), it cannot reach a loopback listener; keep the default bind and restrict access with your firewall or network policy instead.

A mistake in these settings fails at startup with a clear message instead of starting something unintended. The host must be a bare address or hostname (an empty host, a URL like `"http://127.0.0.1"`, a `host:port` pair or whitespace is rejected with `Application.withHost: ...`), and the port must be between 1 and 65535 (`0` and out-of-range values are rejected with `Application.withPort: ...`). If the port is already in use or the address does not belong to this machine, the app stops with `WebTransport could not bind <host>:<port>: ...`.

Your app behaves exactly as before unless you opt in: it still listens on all IPv4 addresses on port 8080. To also accept IPv6 connections, set `Application.withHost "*"`.

This is marked breaking for one narrow case. `WebTransport` and `Application` each gained fields, so code that builds either record by naming every field must add the new ones. Apps that use `WebTransport.server` or `Application.new` are not affected and need no change.

## Migration

Search your code for a `WebTransport { ... }` or `Application { ... }` record written with every field. Most apps have none and can stop here.

- In a `WebTransport { ... }` record, add `host = "*4"` to keep the old behaviour, or `host = "127.0.0.1"` to accept local connections only.
- In an `Application { ... }` record, add `bindHost = Nothing` and `bindPort = Nothing`.

Prefer `WebTransport.server { port = 9090 }` or `Application.withHost` and `Application.withPort` over naming every field.

### Verify

Build your app, then start it and request `/health` on the address you chose. To check the framework change itself, run `./dev test "Service.Transport.Web.Bind" nhcore-test-service`. All 27 examples pass, including one that starts the server on `127.0.0.1`, fetches its health endpoint and confirms the port is still free on the machine's other addresses, and one that shows a port already in use is reported as `could not bind`.

## Agent prompt

```text
My NeoHaskell app fails to compile after upgrading nhcore. `WebTransport` gained a `host :: Text` field and `Application` gained `bindHost :: Maybe Text` and `bindPort :: Maybe Int`. Find every place that builds a `WebTransport { ... }` or `Application { ... }` record by naming all fields (search for `WebTransport {`, `WebTransport$` followed by `{`, and `Application {`). In each `WebTransport` record add `host = "*4"` so it keeps listening on all IPv4 addresses exactly as before. In each `Application` record add `bindHost = Nothing` and `bindPort = Nothing`. Do not change anything that uses `WebTransport.server` or `Application.new`. Keep the port and every other field exactly as they were. Then build the project and run its tests. Report which files you edited and whether the build and tests pass. If my app sits behind a reverse proxy that runs on the same machine or in the same container, tell me I can use `Application.withHost "127.0.0.1"` for local-only access; if the proxy runs elsewhere, tell me to keep the default bind. Do not change the bind host without asking.
```
38 changes: 19 additions & 19 deletions codemap/api-hot.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

- `yield :: value -> Task w value` <!-- 602 call sites -->
- `ignoreError :: Task err Unit -> Task w Unit` <!-- 242 call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 229 call sites -->
- `throw :: err -> Task err w` <!-- 216 call sites -->
- `fromIO :: IO value -> Task w value` <!-- 131 call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 230 call sites -->
- `throw :: err -> Task err w` <!-- 218 call sites -->
- `fromIO :: IO value -> Task w value` <!-- 130 call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 89 call sites -->
- `when :: Bool -> Task err Unit -> Task err Unit` <!-- 31 call sites -->
- `forEach :: (element -> Task err Unit) -> Array element -> Task err Unit` <!-- 30 call sites -->
- `fromFailableIO :: Exception exception => IO result -> Task exception result` <!-- 24 call sites -->
- `fromFailableIO :: Exception exception => IO result -> Task exception result` <!-- 25 call sites -->
- `map :: (input -> output) -> Task err input -> Task err output` <!-- 23 call sites -->

## Array
Expand All @@ -36,7 +36,7 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
- `>>> push 3 (fromLinkedList [1,2] :: Array Int)` → `Array [1,2,3]`
- `takeIf :: (a -> Bool) -> Array a -> Array a` <!-- 31 call sites -->
- `>>> takeIf Basics.isEven (fromLinkedList [1,2,3,4,5,6] :: Array Int)` → `Array [2,4,6]`
- `length :: Array a -> Int` <!-- 29 call sites -->
- `length :: Array a -> Int` <!-- 30 call sites -->
- `>>> length ([1,2,3] :: Array Int)` → `3` …
- `append :: Array a -> Array a -> Array a` <!-- 24 call sites -->
- `>>> (repeat 2 42) |> append (repeat 3 81) :: Array Int` → `Array [42,42,81,81,81]`
Expand All @@ -46,13 +46,13 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"
## Text

- `fromLinkedList :: LinkedList Char -> Text` <!-- 91 call sites -->
- `toLinkedList :: Text -> LinkedList Char` <!-- 78 call sites -->
- `toLinkedList :: Text -> LinkedList Char` <!-- 79 call sites -->
- `toBytes :: Text -> Bytes` <!-- 66 call sites -->
- `replace :: Text -> Text -> Text -> Text` <!-- 33 call sites -->
- `trim :: Text -> Text` <!-- 28 call sites -->
- `trim :: Text -> Text` <!-- 29 call sites -->
- `fromBytes :: Bytes -> Text` <!-- 27 call sites -->
- `isEmpty :: Text -> Bool` <!-- 26 call sites -->
- `joinWith :: Text -> Array Text -> Text` <!-- 26 call sites -->
- `isEmpty :: Text -> Bool` <!-- 25 call sites -->
- `append :: Text -> Text -> Text` <!-- 22 call sites -->
- `toLower :: Text -> Text` <!-- 20 call sites -->

Expand Down Expand Up @@ -130,7 +130,7 @@ codemap/signatures/ · type-directed search: ./dev api "<type>"

## Maybe

- `withDefault :: a -> Maybe a -> a` <!-- 57 call sites -->
- `withDefault :: a -> Maybe a -> a` <!-- 59 call sites -->
- `map :: (a -> b) -> Maybe a -> Maybe b` <!-- 14 call sites -->
- `getOrDie :: HasCallStack => Maybe a -> a` <!-- 9 call sites -->
- `andThen :: (a -> Maybe b) -> Maybe a -> Maybe b` <!-- 4 call sites -->
Expand Down Expand Up @@ -294,13 +294,13 @@ when WRITING TESTS; they are not feature-code frequency signal.

## Task

- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 828 test call sites -->
- `yield :: value -> Task w value` <!-- 483 test call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 286 test call sites -->
- `mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value` <!-- 834 test call sites -->
- `yield :: value -> Task w value` <!-- 487 test call sites -->
- `asResult :: Task err value -> Task err2 (Result err value)` <!-- 292 test call sites -->
- `mapArray :: (element -> Task err output) -> Array element -> Task err (Array output)` <!-- 127 test call sites -->
- `throw :: err -> Task err w` <!-- 124 test call sites -->
- `throw :: err -> Task err w` <!-- 127 test call sites -->
- `fromIO :: IO value -> Task w value` <!-- 106 test call sites -->
- `andThen :: (input -> Task err output) -> Task err input -> Task err output` <!-- 92 test call sites -->
- `fromIO :: IO value -> Task w value` <!-- 90 test call sites -->
- `map :: (input -> output) -> Task err input -> Task err output` <!-- 84 test call sites -->

## Array
Expand All @@ -316,13 +316,13 @@ when WRITING TESTS; they are not feature-code frequency signal.

## Text

- `contains :: Text -> Text -> Bool` <!-- 450 test call sites -->
- `contains :: Text -> Text -> Bool` <!-- 456 test call sites -->
- `toBytes :: Text -> Bytes` <!-- 98 test call sites -->
- `fromLinkedList :: LinkedList Char -> Text` <!-- 58 test call sites -->
- `length :: Text -> Int` <!-- 41 test call sites -->
- `repeat :: Int -> Text -> Text` <!-- 35 test call sites -->
- `toLinkedList :: Text -> LinkedList Char` <!-- 29 test call sites -->
- `startsWith :: Text -> Text -> Bool` <!-- 19 test call sites -->
- `startsWith :: Text -> Text -> Bool` <!-- 21 test call sites -->
- `all :: (Char -> Bool) -> Text -> Bool` <!-- 10 test call sites -->

## Parser
Expand Down Expand Up @@ -371,10 +371,10 @@ when WRITING TESTS; they are not feature-code frequency signal.

## Uuid

- `generate :: Task w Uuid` <!-- 182 test call sites -->
- `generate :: Task w Uuid` <!-- 183 test call sites -->
- `nil :: Uuid` <!-- 108 test call sites -->
- `toText :: Uuid -> Text` <!-- 45 test call sites -->
- `toText :: Uuid -> Text` <!-- 46 test call sites -->
- `generateV5 :: Uuid -> Text -> Uuid` <!-- 19 test call sites -->
- `fromText :: Text -> Maybe Uuid` <!-- 3 test call sites -->

*cut: 140 more modules (Map (260), Service.Application (250), Set (233), AsyncTask (225), Stream (212), …) — full surface: codemap/signatures/*
*cut: 141 more modules (Service.Application (297), Map (260), Set (233), AsyncTask (230), Stream (212), …) — full surface: codemap/signatures/*
16 changes: 13 additions & 3 deletions codemap/signatures/nhcore-service.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ tenantOnly :: (query -> Text) -> Maybe UserClaims -> query -> Maybe AccessError

module Service.Application
ApiInfo :: Text -> Text -> Text -> ApiInfo
Application :: Maybe ConfigSpec -> Maybe EventStoreFactory -> Maybe QueryObjectStoreConfigValue -> Array QueryDefinition -> QueryRegistry -> Array ServiceRunner -> Map Text TransportValue -> Map Text QueryEndpointHandler -> Array OutboundRunner -> Array OutboundLifecycleRunner -> Array Inbound -> Maybe WebAuthFactory -> Maybe OAuth2Setup -> Maybe FileUploadFactory -> Maybe SecretStore -> Maybe ApiInfo -> Maybe CorsConfig -> Maybe HealthCheckConfig -> Maybe DispatcherConfig -> Maybe CorsFactory -> Maybe ApiInfoFactory -> Maybe HealthCheckFactory -> Maybe DispatcherConfigFactory -> Maybe SecretStoreFactory -> Array DeferredOutboundLifecycleReg -> Array DeferredInboundReg -> Array IntegrationRegistrationEntry -> Maybe ReadinessConfig -> Application
Application :: Maybe ConfigSpec -> Maybe EventStoreFactory -> Maybe QueryObjectStoreConfigValue -> Array QueryDefinition -> QueryRegistry -> Array ServiceRunner -> Map Text TransportValue -> Map Text QueryEndpointHandler -> Array OutboundRunner -> Array OutboundLifecycleRunner -> Array Inbound -> Maybe WebAuthFactory -> Maybe OAuth2Setup -> Maybe FileUploadFactory -> Maybe SecretStore -> Maybe ApiInfo -> Maybe CorsConfig -> Maybe HealthCheckConfig -> Maybe DispatcherConfig -> Maybe CorsFactory -> Maybe ApiInfoFactory -> Maybe HealthCheckFactory -> Maybe DispatcherConfigFactory -> Maybe SecretStoreFactory -> Array DeferredOutboundLifecycleReg -> Array DeferredInboundReg -> Array IntegrationRegistrationEntry -> Maybe ReadinessConfig -> Maybe Text -> Maybe Int -> Application
DispatcherConfig :: Int -> Int -> Bool -> Int -> Int -> Maybe Int -> Int -> Int -> DispatcherConfig
FileUploadSetup :: InternalFileUploadConfig -> BlobStore -> FileStateStore -> FileUploadSetup
HealthCheckConfig :: Text -> HealthCheckConfig
Expand All @@ -140,6 +140,8 @@ WebAuthSetup :: Text -> AuthOverrides -> WebAuthSetup
[apiVersion] :: ApiInfo -> Text
[authOverrides] :: WebAuthSetup -> AuthOverrides
[authServerUrl] :: WebAuthSetup -> Text
[bindHost] :: Application -> Maybe Text
[bindPort] :: Application -> Maybe Int
[blobStore] :: FileUploadSetup -> BlobStore
[channelWriteTimeoutMs] :: DispatcherConfig -> Int
[configSpec] :: Application -> Maybe ConfigSpec
Expand Down Expand Up @@ -224,12 +226,14 @@ withDispatcherConfig :: Typeable config => (config -> DispatcherConfig) -> Appli
withEventStore :: (Typeable config, EventStoreConfig eventStoreConfig) => (config -> eventStoreConfig) -> Application -> Application
withFileUpload :: Typeable config => (config -> FileUploadConfig) -> Application -> Application
withHealthCheck :: Typeable config => (config -> Text) -> Application -> Application
withHost :: Text -> Application -> Application
withInbound :: Typeable config => (config -> Inbound) -> Application -> Application
withIntegration :: (Integration request, Typeable request, Typeable (Response request), Inspectable request) => Application -> Application
withOAuth2Provider :: Typeable config => (config -> OAuth2ProviderConfig) -> Application -> Application
withOAuth2StateKey :: Text -> Application -> Application
withOutbound :: (OutboundIntegration handler, entity ~ EntityOf handler, event ~ EventOf entity, event ~ HandledEvent handler, FromJSON entity, FromJSON event, FromJSON (EventOf entity), Inspectable entity, Default entity, Entity entity) => Application -> Application
withOutboundLifecycle :: (Typeable config, Inspectable entity) => (config -> OutboundConfig state) -> Application -> Application
withPort :: Int -> Application -> Application
withQuery :: forall query (queryName :: Symbol) (entities :: [Type]). (Query query, ToSchema query, ToJSON query, FromJSON query, queryName ~ NameOf query, entities ~ EntitiesOf query, KnownSymbol queryName, WireEntities entities query) => Application -> Application
withQueryEndpoint :: Text -> QueryEndpointHandler -> Application -> Application
withQueryObjectStore :: QueryObjectStoreConfig config => config -> Application -> Application
Expand All @@ -248,7 +252,7 @@ withInbound :: Inbound -> (Array OutboundRunner, Array OutboundLifecycleRunner,
withOutboundLifecycle :: Inspectable entity => OutboundConfig state -> (Array OutboundRunner, Array OutboundLifecycleRunner, Array Inbound) -> (Array OutboundRunner, Array OutboundLifecycleRunner, Array Inbound)

module Service.Application.Transports
runTransports :: Map Text TransportValue -> Map Text (Map Text EndpointHandler) -> Map Text (Map Text EndpointSchema) -> Map Text QueryEndpointHandler -> Map Text EndpointSchema -> Maybe AuthEnabled -> Maybe OAuth2Config -> Maybe FileUploadEnabled -> Maybe ApiInfo -> Maybe CorsConfig -> Maybe HealthCheckConfig -> Maybe IntegrationStatus -> Maybe ReadinessConfig -> QuerySubscriber -> Task Text Unit
runTransports :: Map Text TransportValue -> Map Text (Map Text EndpointHandler) -> Map Text (Map Text EndpointSchema) -> Map Text QueryEndpointHandler -> Map Text EndpointSchema -> Maybe AuthEnabled -> Maybe OAuth2Config -> Maybe FileUploadEnabled -> Maybe ApiInfo -> Maybe CorsConfig -> Maybe HealthCheckConfig -> Maybe IntegrationStatus -> Maybe ReadinessConfig -> Maybe Text -> Maybe Int -> QuerySubscriber -> Task Text Unit

module Service.Application.Types
ApiInfo :: Text -> Text -> Text -> ApiInfo
Expand Down Expand Up @@ -1778,7 +1782,7 @@ FileUploadEnabled :: FileUploadRoutes -> Int64 -> FileUploadEnabled
HealthCheckConfig :: Text -> HealthCheckConfig
IntegrationStatus :: !Text -> !Array Text -> IntegrationStatus
OAuth2Config :: OAuth2Routes -> (Text -> Task Text Unit) -> OAuth2Config
WebTransport :: Int -> Int -> Maybe AuthEnabled -> Maybe OAuth2Config -> Maybe FileUploadEnabled -> Maybe ApiInfo -> Maybe CorsConfig -> Maybe HealthCheckConfig -> Maybe IntegrationStatus -> Maybe ReadinessConfig -> Maybe (Task Text Readiness) -> WebTransport
WebTransport :: Int -> Text -> Int -> Maybe AuthEnabled -> Maybe OAuth2Config -> Maybe FileUploadEnabled -> Maybe ApiInfo -> Maybe CorsConfig -> Maybe HealthCheckConfig -> Maybe IntegrationStatus -> Maybe ReadinessConfig -> Maybe (Task Text Readiness) -> WebTransport
[allowedHeaders] :: CorsConfig -> Array Text
[allowedMethods] :: CorsConfig -> Array Text
[allowedOrigins] :: CorsConfig -> Array Text
Expand All @@ -1792,6 +1796,7 @@ WebTransport :: Int -> Int -> Maybe AuthEnabled -> Maybe OAuth2Config -> Maybe F
[fileUploadRoutes] :: FileUploadEnabled -> FileUploadRoutes
[healthCheck] :: WebTransport -> Maybe HealthCheckConfig
[healthPath] :: HealthCheckConfig -> Text
[host] :: WebTransport -> Text
[integrationStatus] :: WebTransport -> Maybe IntegrationStatus
[jwksManager] :: AuthEnabled -> JwksManager
[maxAge] :: CorsConfig -> Maybe Int
Expand All @@ -1803,6 +1808,7 @@ WebTransport :: Int -> Int -> Maybe AuthEnabled -> Maybe OAuth2Config -> Maybe F
[readinessConfig] :: WebTransport -> Maybe ReadinessConfig
[readinessProbe] :: WebTransport -> Maybe (Task Text Readiness)
[routes] :: OAuth2Config -> OAuth2Routes
applyBindOverrides :: Maybe Text -> Maybe Int -> WebTransport -> WebTransport
buildHealthResponse :: WebTransport -> ByteString
data AuthEnabled
data CorsConfig
Expand All @@ -1811,6 +1817,7 @@ data HealthCheckConfig
data IntegrationStatus
data OAuth2Config
data WebTransport
hostPreference :: Text -> HostPreference
instance Data.Record.Anon.Plugin.Internal.Runtime.KnownHash "WebTransport"
instance Service.Transport.Transport Service.Transport.Web.WebTransport
isHealthCheckPath :: Text -> WebTransport -> Bool
Expand All @@ -1820,6 +1827,9 @@ respondReadiness :: WebTransport -> (Response -> Task Text ResponseReceived) ->
server :: WebTransport
unauthorizedResponse :: AccessError -> (Status, Text)
unauthorizedResponseBody :: Text -> Text
validateBindHost :: Text -> Maybe Text
validateBindPort :: Int -> Maybe Text
warpSettings :: WebTransport -> Settings

module Service.Transport.Web.BuiltinSchemas
fileUploadEndpointSchemas :: Bool -> Map Text EndpointSchema
Expand Down
2 changes: 2 additions & 0 deletions core/nhcore.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ test-suite nhcore-test
Service.Transport.Mcp.ResponseSpec
Service.Transport.WebSpec
Service.Transport.Web.SwaggerUISpec
Service.Transport.Web.BindSpec
Service.Transport.Web.HealthCheckSpec
Service.Transport.Web.ReadinessRouteSpec
Service.QueryObjectStore.InMemorySpec
Expand Down Expand Up @@ -630,6 +631,7 @@ test-suite nhcore-test-service
Service.Transport.Mcp.ResponseSpec
Service.MultiTenantSpec
Service.Transport.WebSpec
Service.Transport.Web.BindSpec
Service.Transport.Web.HealthCheckSpec
Service.Transport.Web.ReadinessRouteSpec
Service.Command.AuthSpec
Expand Down
Loading
Loading