kthcloud / kthcloud/console

VM port and custom-domain updates silently fail with an existing custom domain

Open
#370 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.