VM port and custom-domain updates silently fail with an existing custom domain
- Dominant language
- TypeScript
- Stars
- 4
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Description
## Bug issue:
I wasn't able to create more port forwards after the first one, nor change its custom domain settings. It kept saying "saving..." but nothing happened.
I debugged with AI which gave the following writeup (I have confirmed this is the case by using the API directly which worked):
## Summary
The VM port and proxy editors submit read-model port objects as update-model objects. This breaks updates when any existing port has an HTTP proxy with a custom domain.
The relevant API types are structurally different:
```ts
// Read model
interface HttpProxyRead {
name: string;
url?: string;
customDomain?: {
domain: string;
url: string;
status: string;
secret: string;
};
}
// Update model
interface HttpProxyUpdate {
name?: string;
customDomain?: string;
}
```
A TypeScript assertion is being used where an actual read-to-update conversion is required. Assertions do not transform the runtime object, so an untouched custom domain is sent as an object even though the API expects a string.
There is a second issue: `updateVM()` does not check `response.ok`. API validation errors are JSON objects, so they are treated as successful responses, passed to `queueJob()`, and presented as "saving..." instead of showing an error.
## Steps to reproduce
1. Create a VM port-forwarding rule.
2. Add an HTTP proxy with an active custom domain to that rule.
3. Try either of the following:
- add another port-forwarding rule in **Port forwarding**; or
- add/change custom-domain settings for another proxy.
4. Observe that the UI says it is saving, but the change is absent after refreshing.
## Actual behavior
- The frontend submits an invalid update payload containing a read-side `customDomain` object.
- The API rejects the request.
- The frontend treats the JSON error response as a successful job response.
- The UI displays "saving...", but nothing changes.
## Expected behavior
- Every port should be explicitly converted to `PortUpdate` before submission.
- Existing proxies and custom domains should remain unchanged unless explicitly edited.
- Non-2xx responses should display the API error and should not be passed to the job queue.
## PortManager cause
`PortManager` initializes its state directly from `vm.ports`, then adds the new port to that read-model array:
https://github.com/kthcloud/console/blob/main/src/pages/edit/vms/PortManager.tsx#L60-L108
The incompatible array is hidden with:
```ts
ports: newPorts as PortUpdate[]
```
If an existing port has a custom domain, its `CustomDomainRead` object is serialized unchanged.
## ProxyManager cause
`ProxyManager` also starts with the full read-model array:
https://github.com/kthcloud/console/blob/main/src/pages/edit/vms/ProxyManager.tsx#L122-L143
```ts
const portsList = vm.ports as PortCreate[];
```
It reconstructs only the selected proxy. Other proxies remain read objects, after which the complete array is asserted as `PortUpdate[]`.
For the selected proxy, `customDomain` is correctly sent as a string. However, an untouched proxy can still be sent like this:
```json
{
"httpProxy": {
"name": "existing-proxy",
"url": "https://existing-proxy.vm-app.cloud.cbh.kth.se",
"customDomain": {
"domain": "existing.example.com",
"url": "https://existing.example.com",
"status": "active",
"secret": "..."
}
}
}
```
The entire update is then rejected because `HttpProxyUpdate.customDomain` expects a string.
`ProxyManager` also mutates objects from `vm.ports` directly before the request completes.
## Error-handling cause
`updateVM()` parses the response but never checks its HTTP status:
https://github.com/kthcloud/console/blob/main/src/api/deploy/vms.ts#L68-L83
Conceptually it does:
```ts
const result = await response.json();
if (typeof result !== "object") throw new Error(...);
return result;
```
A validation error is an object, so it is returned as though the update succeeded.
## Suggested fix
1. Add an explicit `PortRead` to `PortUpdate` mapper and remove the unsafe casts.
2. Build new objects rather than mutating `vm.ports`.
3. Include only writable fields: `name`, `port`, `protocol`, and writable `httpProxy` fields.
4. For an unchanged existing proxy, send its `name` but omit `customDomain`. The backend treats omission as unchanged. Re-sending an unchanged domain string may regenerate its verification secret and return it to pending state.
5. Send a domain string only when it is explicitly added or changed; send the API's deletion representation only when explicitly removed.
6. Make `updateVM()` throw the parsed API error whenever `!response.ok`.
For example, preserving an existing proxy while updating the VM should produce an entry shaped like:
```ts
{
name: port.name,
port: port.port,
protocol: port.protocol,
httpProxy: port.httpProxy
? { name: port.httpProxy.name }
: undefined,
}
```
## Acceptance criteria
- A second port can be added when another port has an active custom domain.
- Proxy/custom-domain settings can be changed when another proxy has an active custom domain.
- Existing custom domains retain their domain, verification status, and secret when unrelated ports are edited.
- Requests contain no read-only `externalPort`, proxy `url`, custom-domain `status`, or custom-domain `secret` fields.
- Non-2xx responses show a useful error and are never passed to `queueJob()`.
- Tests cover both `PortManager` and `ProxyManager` with an existing active custom-domain proxy.
Contributor guide
No contributing guide indexed for this repository
Research direction
Read src/pages/edit/vms/PortManager.tsx and ProxyManager.tsx to trace how vm.ports becomes the update payload, then inspect src/api/deploy/vms.ts for response handling. Add coverage for both managers with an existing active custom-domain proxy. Done means unrelated edits preserve existing domains and non-2xx responses show an error without entering the job queue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- api, frontend, testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100