Skip to content

Angular UI requests /server/api without Accept: application/hal+json after Angular 20 upgrade (no _links) #4959

@bram-atmire

Description

@bram-atmire

Describe the bug

When running the DSpace 10 Angular UI after the recent upgrade to Angular 20, the UI fails to bootstrap against a standard DSpace 10 REST backend with the error:

No _links section found at http://localhost:8080/server/api

The DSpace REST API root endpoint returns HAL JSON correctly when requested explicitly with Accept: application/hal+json, but the UI’s browser requests default to Accept: application/json, text/plain, */*, which triggers a non-HAL response (Content-Type: application/json) without _links. This breaks the UI because it requires HAL links from the API root for discovery/navigation.

This appears to be a regression introduced during the Angular 20 upgrade because the current DspaceRestInterceptor only rewrites SSR base URLs and does not set the required HAL Accept header for REST requests.

Observed in:

  • DSpace Angular UI: dspace-angular@10.0.0-next (Angular 20.x)
  • DSpace REST API: DSpace 10 REST backend (local install)
  • Browser: Chrome (also reproducible in other Chromium-based browsers)

To Reproduce

Steps to reproduce the behavior:

  1. Set up a local DSpace 10 REST API, running on:
    • http://localhost:8080/server/api
  2. Set up and build the DSpace Angular UI from the current Angular 20 branch:
    • npm install
    • npm run build:prod
    • start SSR (for example via npm run serve:ssr or via PM2 using dist/server/main.js)
  3. Open the UI in a browser (e.g. Chrome).
  4. Observe the UI fails to bootstrap and logs:
    • No _links section found at http://localhost:8080/server/api
  5. Inspect the network request for GET http://localhost:8080/server/api:
    • Request header Accept is: application/json, text/plain, */*
    • Response header Content-Type is: application/json;charset=UTF-8
    • Response body does not contain _links
  6. Compare with calling the same endpoint explicitly requesting HAL:
    curl -i -H "Accept: application/hal+json" http://localhost:8080/server/api
    • Response is 200 OK
    • Content-Type: application/hal+json;charset=UTF-8
    • Response contains _links

Expected behavior

The Angular UI should request HAL responses from the REST API root and other REST endpoints, i.e. it should send an Accept: application/hal+json header (at least for /server/api and likely for all REST calls) so that the API returns a HAL response containing _links.

This would prevent the UI from failing during startup and maintain the expected HAL-driven API discovery mechanism.

Suggested fix (code snippet)

One potential fix is to ensure REST requests explicitly request HAL responses by setting Accept: application/hal+json for requests targeting the REST API base URL, while not overriding any explicitly set Accept header (e.g. auth requests that may set application/json intentionally).

Example patch to src/app/core/dspace-rest/dspace-rest.interceptor.ts:

import { isPlatformBrowser } from '@angular/common';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
} from '@angular/common/http';
import {
  Inject,
  Injectable,
  PLATFORM_ID,
} from '@angular/core';
import {
  APP_CONFIG,
  AppConfig,
} from '@dspace/config/app-config.interface';
import { isEmpty } from '@dspace/shared/utils/empty.util';
import { Observable } from 'rxjs';

@Injectable()
export class DspaceRestInterceptor implements HttpInterceptor {

  protected baseUrl: string;
  protected ssrBaseUrl: string;

  constructor(
    @Inject(APP_CONFIG) protected appConfig: AppConfig,
    @Inject(PLATFORM_ID) private platformId: string,
  ) {
    this.baseUrl = this.appConfig.rest.baseUrl;
    this.ssrBaseUrl = this.appConfig.rest.ssrBaseUrl;
  }

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {

    let newRequest = request;

    // If this is a REST API request and no Accept header was explicitly set, request HAL
    const isRestRequest =
      request.url.startsWith(this.baseUrl) ||
      (!isEmpty(this.ssrBaseUrl) && request.url.startsWith(this.ssrBaseUrl));

    if (isRestRequest && !request.headers.has('Accept')) {
      newRequest = newRequest.clone({
        setHeaders: {
          Accept: 'application/hal+json',
        },
      });
    }

    // Existing SSR URL rewriting logic
    if (!isPlatformBrowser(this.platformId) && !isEmpty(this.ssrBaseUrl) && this.baseUrl !== this.ssrBaseUrl) {
      const url = newRequest.url.replace(this.baseUrl, this.ssrBaseUrl);
      newRequest = newRequest.clone({ url });
    }

    return next.handle(newRequest);
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugneeds triageNew issue needs triage and/or scheduling

    Type

    No type

    Projects

    Status

    🆕 Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions