You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by jo...@apache.org on 2026/08/07 03:43:47 UTC
(zeppelin) branch master updated: [ZEPPELIN-6521] Preserve withCredentials on HTTP requests in production builds
This is an automated email from the ASF dual-hosted git repository.
jongyoul pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new f988eef9fe [ZEPPELIN-6521] Preserve withCredentials on HTTP requests in production builds
f988eef9fe is described below
commit f988eef9fe9a2853988193c7572c3b01d554441c
Author: dae won <99...@users.noreply.github.com>
AuthorDate: Fri Aug 7 12:43:40 2026 +0900
[ZEPPELIN-6521] Preserve withCredentials on HTTP requests in production builds
### What is this PR for?
In production builds the interceptor re-clones from the original `httpRequest` when it adds `X-Requested-With`, discarding the `withCredentials: true` clone made one line earlier. `clone()` inherits from whatever it was cloned from (`update.withCredentials ?? this.withCredentials`), so the production request goes out with the default `false`.
```diff
let httpRequestUpdated = httpRequest.clone({ withCredentials: true });
if (environment.production) {
- httpRequestUpdated = httpRequest.clone({ setHeaders: { 'X-Requested-With': 'XMLHttpRequest' } });
+ httpRequestUpdated = httpRequestUpdated.clone({ setHeaders: { 'X-Requested-With': 'XMLHttpRequest' } });
}
```
Two things the ticket does not cover. First, `BaseUrlService` builds the REST base from `location`, so every call to Zeppelin's own API is same-origin and the browser attaches cookies whether or not the flag is set; the flag has no reachable effect on those calls today. The classic UI's equivalent service remaps the port when the UI is served from the grunt dev server (`zeppelin-web/src/components/base-url/base-url.service.js:26-29`), which is what made `withCredentials` meaningful th [...]
### Scope and related issues
**Third-party URL fetches go through this interceptor too, and this change narrows what they can reach.** `NoteImportComponent` passes a user-supplied URL straight to `HttpClient` (`note-import.component.ts:49`), so the "import note from URL" request carries the same `withCredentials` and `X-Requested-With` as a call to Zeppelin's own API. A browser rejects a credentialed cross-origin response whose `Access-Control-Allow-Origin` is `*`, and that wildcard is what public file hosts serve.
Measured in Chromium against local servers reproducing each CORS configuration, with the `raw.githubusercontent.com` preflight response checked directly:
| target host | dev build | production today | production after this PR |
|---|---|---|---|
| rejects `X-Requested-With` at preflight — `raw.githubusercontent.com` answers the preflight with 403 | blocked | blocked | blocked |
| allows `X-Requested-With`, serves `Access-Control-Allow-Origin: *` | blocked | works | blocked |
Only the second row changes, and it changes production to match what development already does, which is what this ticket asks for.
The underlying problem is that the interceptor does not distinguish Zeppelin's own API from an arbitrary URL. The classic UI handles it by overriding `withCredentials: false` for exactly this request (`zeppelin-web/src/components/note-import/note-import.controller.js:95-97`); the new UI has no equivalent. That is a separate defect which predates this ticket and already breaks the feature in development builds today.
Scoping the interceptor so that `withCredentials` and `X-Requested-With` are applied only to Zeppelin's own API would resolve it, and would not change anything this PR does for API calls. I have not filed a ticket for it yet — I would rather hear whether that scoping belongs in this PR or in a follow-up.
### What type of PR is it?
Bug Fix
### Todos
* [x] - Derive the production clone from the already-credentialed request
* [x] - Confirm `X-Requested-With` is still added in production builds
* [x] - Confirm development builds are unaffected
### What is the Jira issue?
* [ZEPPELIN-6521](https://issues.apache.org/jira/browse/ZEPPELIN-6521)
### How should this be tested?
There is no unit-test runner for this app to add a spec to: the `zeppelin` project in `angular.json` declares only `build`, `serve`, `extract-i18n` and `lint`, and `zeppelin-web-angular/src` contains no `.spec.ts`. A Playwright test is also a poor fit here, because the local suite runs against `ng serve` (`playwright.config.js:16`), where `environment.production` is `false` and the affected branch never executes — such a test would pass with or without this change unless run in CI mod [...]
**1. Lint**
```
cd zeppelin-web-angular && npm run lint
```
Exit 0. 15 pre-existing `member-ordering` warnings in `projects/zeppelin-visualization`, none in the changed file; `lint:react` clean; `prettier --check` reports all files formatted.
**2. `HttpRequest.clone()` inheritance, using the real class**
Running both the old and the new form of the production branch through Angular's real `HttpRequest` class:
```
development branch withCredentials=true X-Requested-With=(none)
production, before withCredentials=false X-Requested-With=XMLHttpRequest
production, after withCredentials=true X-Requested-With=XMLHttpRequest
```
**3. Production bundle in a real browser**
`ng build --configuration production` twice — once with this commit and once with it reverted, changing nothing else — serving `dist/zeppelin` statically and instrumenting the `XMLHttpRequest.prototype.withCredentials` setter. `BaseUrlService` derives the REST base from `location`, so the app issues its normal bootstrap calls against the static server; they 404, but Angular assigns `withCredentials` before `send()`, which is what is being observed.
```
API requests withCredentials assignments X-Requested-With
before this commit 4 [] XMLHttpRequest
after this commit 4 [true, true, true, true] XMLHttpRequest
```
`X-Requested-With` is present in both runs, which confirms the production branch really executed and that the change does not drop the header.
### Screenshots (if appropriate)
N/A
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No — see "Scope and related issues" for the one behaviour that changes for third-party URL fetches
* Does this needs documentation? No
Closes #5377 from big-cir/ZEPPELIN-6521.
Signed-off-by: Jongyoul Lee <jo...@gmail.com>
---
.../share/note-import/note-import-modal.spec.ts | 42 ++++++++++++++++++++++
.../src/app/app-http.interceptor.ts | 2 +-
.../app/share/note-import/note-import.component.ts | 8 +++--
3 files changed, 48 insertions(+), 4 deletions(-)
diff --git a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts
index 229967d719..6361759afd 100644
--- a/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/share/note-import/note-import-modal.spec.ts
@@ -10,6 +10,8 @@
* limitations under the License.
*/
+import { createServer } from 'node:http';
+
import { test, expect } from '@playwright/test';
import { HomePage } from '../../../models/home-page';
import { NoteImportModal } from '../../../models/note-import-modal';
@@ -69,6 +71,46 @@ test.describe('Note Import Modal', () => {
await expect(noteImportModal.importNoteButton).toBeEnabled();
});
+ test('Given URL tab is selected, When importing from wildcard CORS origin, Then response should be readable', async () => {
+ const corsServer = createServer((request, response) => {
+ response.setHeader('Access-Control-Allow-Headers', 'X-Requested-With');
+ response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
+ response.setHeader('Access-Control-Allow-Origin', '*');
+ response.setHeader('Connection', 'close');
+
+ if (request.method === 'OPTIONS') {
+ response.writeHead(204);
+ response.end();
+ return;
+ }
+
+ response.setHeader('Content-Type', 'application/json');
+ response.end(JSON.stringify({ name: 'Missing paragraphs' }));
+ });
+
+ await new Promise<void>((resolve, reject) => {
+ corsServer.once('error', reject);
+ corsServer.listen(0, '127.0.0.1', resolve);
+ });
+
+ try {
+ const address = corsServer.address();
+ if (!address || typeof address === 'string') {
+ throw new Error('Failed to bind CORS test server');
+ }
+
+ await noteImportModal.switchToUrlTab();
+ await noteImportModal.setImportUrl(`http://127.0.0.1:${address.port}/note.json`);
+ await noteImportModal.clickImportNote();
+
+ await expect(noteImportModal.errorAlert).toHaveText('Invalid JSON');
+ } finally {
+ await new Promise<void>((resolve, reject) => {
+ corsServer.close(error => (error ? reject(error) : resolve()));
+ });
+ }
+ });
+
test('Given Import Note modal is open, When entering import name, Then name should be set', async () => {
const importName = `Imported Note ${Date.now()}`;
await noteImportModal.setImportAsName(importName);
diff --git a/zeppelin-web-angular/src/app/app-http.interceptor.ts b/zeppelin-web-angular/src/app/app-http.interceptor.ts
index b1e287622a..6a6a4a1853 100644
--- a/zeppelin-web-angular/src/app/app-http.interceptor.ts
+++ b/zeppelin-web-angular/src/app/app-http.interceptor.ts
@@ -28,7 +28,7 @@ export class AppHttpInterceptor implements HttpInterceptor {
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let httpRequestUpdated = httpRequest.clone({ withCredentials: true });
if (environment.production) {
- httpRequestUpdated = httpRequest.clone({ setHeaders: { 'X-Requested-With': 'XMLHttpRequest' } });
+ httpRequestUpdated = httpRequestUpdated.clone({ setHeaders: { 'X-Requested-With': 'XMLHttpRequest' } });
}
return next.handle(httpRequestUpdated).pipe(
map(event => {
diff --git a/zeppelin-web-angular/src/app/share/note-import/note-import.component.ts b/zeppelin-web-angular/src/app/share/note-import/note-import.component.ts
index a73627b9ba..ef5fdb7f40 100644
--- a/zeppelin-web-angular/src/app/share/note-import/note-import.component.ts
+++ b/zeppelin-web-angular/src/app/share/note-import/note-import.component.ts
@@ -10,7 +10,7 @@
* limitations under the License.
*/
-import { HttpClient } from '@angular/common/http';
+import { HttpBackend, HttpClient } from '@angular/common/http';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ConfigurationService, MessageService, TicketService } from '@zeppelin/services';
@@ -37,6 +37,7 @@ export class NoteImportComponent extends MessageListenersManager implements OnIn
errorText?: string;
importLoading = false;
wsMaxLimit?: number;
+ private readonly externalHttpClient: HttpClient;
@MessageListener(OP.IMPORT_NOTE)
noteImported(_: MessageReceiveDataTypeMap[OP.IMPORT_NOTE]) {
@@ -46,7 +47,7 @@ export class NoteImportComponent extends MessageListenersManager implements OnIn
importNote() {
this.errorText = '';
this.importLoading = true;
- this.httpClient.get(this.importUrl ?? '').subscribe(
+ this.externalHttpClient.get(this.importUrl ?? '').subscribe(
data => {
this.importLoading = false;
this.processImportJson(data);
@@ -106,9 +107,10 @@ export class NoteImportComponent extends MessageListenersManager implements OnIn
private configurationService: ConfigurationService,
private cdr: ChangeDetectorRef,
private nzModalRef: NzModalRef,
- private httpClient: HttpClient
+ httpBackend: HttpBackend
) {
super(messageService);
+ this.externalHttpClient = new HttpClient(httpBackend);
}
async ngOnInit() {