Skip to content

[MM-67557] Handle HTML responses on API calls#9922

Open
larkox wants to merge 4 commits into
mainfrom
MM-67557
Open

[MM-67557] Handle HTML responses on API calls#9922
larkox wants to merge 4 commits into
mainfrom
MM-67557

Conversation

@larkox

@larkox larkox commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

When we try to get something from the API, an HTML response is considered a success. This then fails down the line, since we try to access the data of the response, and it happens to be something unexpected.

This was specially confusing when adding a server using the full route to, for example, a channel. In that scenario, the ping will succeed, the fetch of config and license would also succeed, but when trying to access to the diagnostic id in the license, it would fail.

So this PR handles two things here:

  • If the server is reporting returning an HTML response, we reject the request
  • When adding a new server, on ping, if we receive a 406 (this behavior is added in the related server PR), we use the base url on the response to create the server.

Ticket Link

Fix https://mattermost.atlassian.net/browse/MM-67557

Related PRs

Server: TBD

Release Note

When creating a server, we now handle properly when the server includes channel or post links

@mattermost-build mattermost-build added the E2E/Run Triggers E2E tests on both iOS and Android via Matterwick label Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 237cb499-64a3-4ecf-9cb8-24b2d0a0578c

📥 Commits

Reviewing files that changed from the base of the PR and between b50750f and beb2d33.

📒 Files selected for processing (1)
  • app/utils/url/index.ts
💤 Files with no reviewable changes (1)
  • app/utils/url/index.ts

📝 Walkthrough

Walkthrough

The change validates response content, preserves response details, retries 406 pings using sanitized base URLs, returns effective server URLs, propagates redirected URLs through login, and adds unit and Detox coverage.

Changes

Server connection flow

Layer / File(s) Summary
Response validation and error details
app/client/rest/tracking.ts, app/client/rest/tracking.test.ts
Successful HTML responses now raise an invalid-response error, while non-success errors include response data in ClientError.details; tests cover both behaviours.
Ping result and base URL retry handling
app/utils/url/index.ts, app/actions/remote/general.ts, app/actions/remote/general.test.ts
doPing returns serverUrl, detects pre-auth errors, and retries HTTP 406 responses once using a sanitized base_url, with coverage for response and thrown-error paths.
Redirected URL propagation through login
app/screens/server/index.tsx
The effective ping URL is used for configuration, security checks, credentials, login navigation, notifications, and URL state.
Redirected URL UI coverage
detox/e2e/support/ui/screen/server.ts, detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts
The server screen gains platform-specific URL assertions, and a skipped Detox test covers base URL rewriting during server login.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ServerScreen
  participant doPing
  participant NetworkManager
  participant RedirectedServer
  ServerScreen->>doPing: ping original server URL
  doPing->>NetworkManager: create client for original URL
  NetworkManager->>RedirectedServer: send ping
  RedirectedServer-->>doPing: HTTP 406 with base_url
  doPing->>NetworkManager: create client for sanitized base_url
  NetworkManager->>RedirectedServer: retry ping
  RedirectedServer-->>doPing: successful ping
  doPing-->>ServerScreen: return serverUrl
Loading

Suggested labels: kind/bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly reflects one of the main changes: handling HTML responses on API calls.
Description check ✅ Passed The description matches the PR scope by covering HTML-response rejection and 406 base-url retry handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MM-67557

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/screens/server/index.tsx (1)

331-335: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer ?? for the serverUrl fallback.

result.serverUrl is string | undefined; using ?? is more precise than || for this fallback (avoids accidentally treating a falsy-but-valid string as absent).

As per coding guidelines: "In TypeScript, use ?? instead of || for fallbacks when appropriate."

♻️ Proposed fix
-        const serverUrlToUse = result.serverUrl || headRequest.url;
+        const serverUrlToUse = result.serverUrl ?? headRequest.url;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/screens/server/index.tsx` around lines 331 - 335, Replace the `||`
fallback in the `serverUrlToUse` assignment with `??`, preserving
`result.serverUrl` when it is defined and falling back to `headRequest.url` only
when it is nullish.

Source: Path instructions

detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts (1)

42-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider reusing ServerScreen.dismissIosNotificationsAlert() instead of a bespoke inline dismissal.

This block reimplements a simplified version of the alert-dismissal logic already handled robustly (multiple title/label variants, retry strategies) by dismissIosNotificationsAlert() in detox/e2e/support/ui/screen/server.ts. Reusing it would avoid duplicated, less resilient logic in this test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts`
around lines 42 - 49, The inline alert dismissal logic in the if block (checking
isIos() and !process.env.CI) duplicates functionality already provided by
ServerScreen.dismissIosNotificationsAlert() from
detox/e2e/support/ui/screen/server.ts. Replace the entire try-catch block that
waits for and taps Alert.okayButton with a single call to
ServerScreen.dismissIosNotificationsAlert(). This will use the existing, more
robust implementation that handles multiple title and label variants and
includes proper retry strategies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/actions/remote/general.ts`:
- Around line 91-140: Normalize the extracted base URL before both 406 retry
calls in doPing: after getBaseUrlFromResponseData returns a value, remove any
trailing slash while preserving the rest of the URL, then pass the normalized
value to doPing. Apply this consistently in both the response-handling branch
and the caught ClientError branch so NetworkManager.createClient and the
returned serverUrl receive the normalized URL.

In `@detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts`:
- Line 36: Replace the placeholder test identifier in the skipped test name
within the `base_url_redirect` suite with the assigned tracking ticket using the
required `MM-TXXXX_N` format, preserving the test description.

---

Nitpick comments:
In `@app/screens/server/index.tsx`:
- Around line 331-335: Replace the `||` fallback in the `serverUrlToUse`
assignment with `??`, preserving `result.serverUrl` when it is defined and
falling back to `headRequest.url` only when it is nullish.

In `@detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts`:
- Around line 42-49: The inline alert dismissal logic in the if block (checking
isIos() and !process.env.CI) duplicates functionality already provided by
ServerScreen.dismissIosNotificationsAlert() from
detox/e2e/support/ui/screen/server.ts. Replace the entire try-catch block that
waits for and taps Alert.okayButton with a single call to
ServerScreen.dismissIosNotificationsAlert(). This will use the existing, more
robust implementation that handles multiple title and label variants and
includes proper retry strategies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71487aa3-f4c7-4a82-9996-05b368c6ec42

📥 Commits

Reviewing files that changed from the base of the PR and between f7f8ee7 and 9166e49.

📒 Files selected for processing (7)
  • app/actions/remote/general.test.ts
  • app/actions/remote/general.ts
  • app/client/rest/tracking.test.ts
  • app/client/rest/tracking.ts
  • app/screens/server/index.tsx
  • detox/e2e/support/ui/screen/server.ts
  • detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts

Comment thread app/actions/remote/general.ts
});

// Skipping until the server is updated to return the base URL in the 406 response
it.skip('MM-TBD_1 - should rewrite server URL from 406 base_url response and connect', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Placeholder ticket number in test name.

MM-TBD_1 doesn't match the required MM-TXXXX_N format. Update once the tracking ticket exists.

As per coding guidelines: "Name test cases with MM-TXXXX_N where XXXX is the Jira/Zephyr ticket number."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts` at
line 36, Replace the placeholder test identifier in the skipped test name within
the `base_url_redirect` suite with the assigned tracking ticket using the
required `MM-TXXXX_N` format, preserving the test description.

Source: Path instructions

@mattermost-build mattermost-build removed the E2E/Run Triggers E2E tests on both iOS and Android via Matterwick label Jul 10, 2026
@mattermost-build mattermost-build added the E2E/Run Triggers E2E tests on both iOS and Android via Matterwick label Jul 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/utils/url/index.ts (1)

55-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Redundant else if branch — collapse the protocol selection.

With the new logic, the middle branch is dead code. When keepProtocol is false and !useHttp, the final else already yields 'https:' — the exact value the else if (preUrl.protocol === 'http:' && !useHttp) branch returns. When useHttp is true, that middle condition never fires. So for every input the else produces the same result, making the branch a duplicate conditional.

As per coding guidelines: "Avoid redundant checks, impossible states, duplicate conditional branches that return the same result, and other over-engineered control flow."

♻️ Proposed simplification
     let protocol;
     if (keepProtocol) {
         protocol = preUrl.protocol;
-    } else if (preUrl.protocol === 'http:' && !useHttp) {
-        protocol = 'https:';
     } else {
         protocol = useHttp ? 'http:' : 'https:';
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/utils/url/index.ts` around lines 55 - 62, Remove the redundant `else if
(preUrl.protocol === 'http:' && !useHttp)` branch in the protocol selection
logic, retaining the `keepProtocol` check and using the final fallback to assign
`useHttp ? 'http:' : 'https:'`.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/utils/url/index.ts`:
- Around line 55-62: Remove the redundant `else if (preUrl.protocol === 'http:'
&& !useHttp)` branch in the protocol selection logic, retaining the
`keepProtocol` check and using the final fallback to assign `useHttp ? 'http:' :
'https:'`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e853d506-2a0d-4399-9808-4fcc70644b0c

📥 Commits

Reviewing files that changed from the base of the PR and between 9166e49 and b50750f.

📒 Files selected for processing (3)
  • app/actions/remote/general.ts
  • app/utils/url/index.ts
  • detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts
✅ Files skipped from review due to trivial changes (1)
  • detox/e2e/test/products/channels/server_login/base_url_redirect.e2e.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/actions/remote/general.ts

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Coverage Comparison Report

Generated on July 13, 2026 at 13:25:45 UTC

+-----------------+------------+------------+-----------+
| Metric          | Main       | This PR    | Diff      |
+-----------------+------------+------------+-----------+
| Lines           |     87.75% |     87.79% |     0.04% |
| Statements      |     87.62% |     87.66% |     0.04% |
| Branches        |     76.58% |     76.63% |     0.05% |
| Functions       |     87.21% |     87.26% |     0.05% |
+-----------------+------------+------------+-----------+
| Total           |     84.79% |     84.83% |     0.04% |
+-----------------+------------+------------+-----------+

@mattermost-build mattermost-build removed the E2E/Run Triggers E2E tests on both iOS and Android via Matterwick label Jul 10, 2026
@mattermost-build mattermost-build added the E2E/Run Triggers E2E tests on both iOS and Android via Matterwick label Jul 13, 2026
@larkox
larkox requested review from a team and hmhealey and removed request for a team July 13, 2026 15:58
@mattermost-build mattermost-build removed the E2E/Run Triggers E2E tests on both iOS and Android via Matterwick label Jul 13, 2026

function getBaseUrlFromResponseData(data: unknown): string | undefined {
if (data && typeof data === 'object' && 'base_url' in data) {
const baseUrl = data.base_url;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure about adding this base_url to the server response? I wouldn't expect the server to return anything like that for a 406 error, so I'd expect something like we trim the path from serverUrl. If we want to fully support subpaths with that, we could even retry multiple times, each time with one more path segment removed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context. I'm still not sure if I'd expect that since there's other reasons someone might accidentally request JSON from another endpoint, but I guess that makes sense when you compare to how we return the web app for any non-API URL

if (response.code === 406 && !hasRetriedBaseUrl) {
const baseUrl = getBaseUrlFromResponseData(response.data);
if (baseUrl) {
return doPing(sanitizeUrl(baseUrl, false, true), verifyPushProxy, timeoutInterval, preauthSecret, undefined, true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any chance that this throws an error that's caught by the outer try/catch?

If it can and client is undefined, it looks like we might call NetworkManager.invalidateClient twice if that matters

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In reality, the normal path is going to be the try catch. We have this as defensive coding.

Invalidating the same client twice is not an issue.

const errorObj = error as ClientError;

// Check if this is a 406 with base_url in the response data
if (errorObj.status_code === 406 && !hasRetriedBaseUrl) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we handling this in both the try and catch blocks? Should we perhaps take these out of the try/catch?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the current pattern. Not great, but I didn't want to deviate from it.

await wait(timeouts.ONE_SEC);
};

waitForServerUrl = async (expectedUrl: string, timeout = timeouts.TEN_SEC) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these branches supposed to be the same?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what do you mean here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I thought the await lines were both identical, but I missed that one is toHaveValue and the other is toHaveText

@larkox
larkox requested a review from hmhealey July 16, 2026 09:31
@larkox
larkox requested a review from lieut-data July 17, 2026 08:47

if (response.ok) {
const contentType = getResponseHeader(headers, 'Content-Type');
if (contentType?.toLowerCase().includes('text/html')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why test for text/html instead of asserting application/json?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in case in any API we are sending a empty content type, or anything similar. But you are probably right, and we should go with the application/json one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants