Universal: Loading an assets file with relative path

Created on 5 Dec 2017  路  12Comments  路  Source: angular/universal

  • I'm submitting a ...
- [X ] bug report
- [ ] feature request
- [ ] support request 
  • What modules are related to this Issue?
- [ ] aspnetcore-engine
- [x ] express-engine
- [ ] hapi-engine
  • Do you want to request a feature or report a bug?
    Bug

  • What is the current behavior?
    When trying to load assets files (json) from the assets folder with a relative path, the Universal version of our application fails to load them. The client version works fine. The files are intended to load via a service which is initiated via APP_INITIALIZER. The service starts up and works fine, just the files cant be loaded. Unless you load them with an absolute path (http://localhost:4000/assets...)

  • If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem by creating a github repo.

  • Create a service to load json-files via HttpClient.
  • Call it via APP_INITIALIZER
  • Turn on Universal according to https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/universal-rendering.md

  • What is the expected behavior?
    The expected behaviour would be that the files load as they do in the client version

  • Please tell us about your environment:

  • Angular version: 5.0

  • Browser: [all]
  • OS: [Mac OS X | Windows ]
  • Platform: [all | NodeJs ]

All 12 comments

Have the same issue. @patrickmichalina Can you explain how you solved it in the example? I see an interceptor that changes the url url:${this.env.config.host}/${req.url.replace('./', '')}` on each request. If I have understood properly, this does not fix the fact that each request needs to have an absolute path.

Can this be added to the Gotchas as long as it's not fixed?

This should be added to the gotchas you're right @OCCGU

@nekkon @phihochzwei
Patrick is right, so you need absolute paths/urls for API requests / etc.

The Node server can't assume (and has no concept) of relative folders & paths, so it needs the entire path to be able to find something.

I hope that helps!

I give up... I been trying to do this, but with the prerender script. I made the interceptor, but there is actually no server to ask the resource, so really have no idea where or who this should be asked.

I tried with file protocol, but the application always send a option request first, and file only supports get request. (And two line after that there is a throw with File protocol not supported, so I guess that is a no-go).

I think the prerender scenario is actually the one that could benefice more about this loading, and I understand that when the script is interpreted, something really special have to happen to load a file that is somewhere else.

@michaeljota: I'm not sure this is the best way to solve it, but I didn't use intereceptors for http calls in my solution.

Instead, I created a copy of the included node server script, and call the prerender script in the end like this:

// Start up the Node server
const server = app.listen(PORT, () => {
  runScript(join(DIST_FOLDER, 'prerender.js'), function (err) {
    if (err) { throw err; }
    server.close();
  });
});

function runScript(scriptPath, callback) {

  // keep track of whether callback has been invoked to prevent multiple invocations
  let invoked = false;

  const process = childProcess.fork(scriptPath);

  // listen for errors as they may prevent the exit event from firing
  process.on('error', function (err) {
    if (invoked) { return; }
    invoked = true;
    callback(err);
  });

  // execute the callback once the process has finished running
  process.on('exit', function (code) {
    if (invoked) { return; }
    invoked = true;
    const err = code === 0 ? null : new Error('exit code ' + code);
    callback(err);
  });
}

This way, the server gets started, opens the prerender and closes the server once the prerender script finishes.

The constructor for my communication service checks if it's executed in a browser, and if that is not the case, it prepends relative file paths with http://localhost:8080, thus receiving the needed files from the server.

@OCCGU Thanks! Will try that. I don't know why this have to be so complicated.

EDIT: I understand why this have to be so complicated, but I don't get why this _still_ have to be so complicated.

EDIT: If you would like for some reason, use promises with @OCCGU example:

// Start up the Node server
const server = app.listen(PORT, async () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
  try {
    await runScript('./prerender.js');
    console.log('Prerender finished successfully');
  } catch (err) {
    throw err;
  } finally {
    server.close();
  }
});

function runScript(scriptPath) {
  return new Promise((resolve, reject) => {
    fork(scriptPath)
      .on(
        'error',
        // listen for errors as they may prevent the exit event from firing
        function handleError(err) {
          console.error(err);
        },
      )
      .on(
        'exit',
        // resolve the promise once the process has finished running
        function handleExit(code) {
          if (code === 0) {
            resolve();
          }
          reject(new Error(`exit code ${code}`));
        },
      );
  });
}

Now, the original issue because I forgot how to handle promises for a second.
I did what you said, but I still have some issues. I modify the code to understand it and see if I could do something, but I can't actually see it.

// Start up the Node server
const server = app.listen(PORT, async () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
  try {
    await runScript('./prerender.js');
  } catch (err) {
    throw err;
  } finally {
    server.close();
  }
});

async function runScript(scriptPath) {
  fork(scriptPath)
    .on(
      'error',
      // listen for errors as they may prevent the exit event from firing
      function handleError(err) {
        throw err;
      },
    )
    .on(
      'exit',
      // execute the callback once the process has finished running
      function handleExit(code) {
        if (code === 0) {
          return;
        }
        throw new Error(`exit code ${code}`);
      },
    );
}

EDIT: For some reason, if I left the server running forever it works. Maybe its something about how I'm handling the process.

working code:

const server = app.listen(PORT, async () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
  try {
    await runScript('./prerender.js');
    // server.close();
  } catch (err) {
    throw err;
  } finally {
  }
  console.log('Running');
});

async function runScript(scriptPath) {
  fork(scriptPath)
    .on(
      'error',
      // listen for errors as they may prevent the exit event from firing
      function handleError(err) {
        throw err;
      },
    )
    .on(
      'exit',
      // execute the callback once the process has finished running
      function handleExit(code) {
        if (code === 0) {
          return;
        }
        throw new Error(`exit code ${code}`);
      },
    );
}

I try removing the throwing in handleError and try moving the close method inside the finally, after the awaiting of the process, and after the try-catch. Nothing worked for me. But I hope this can be base for someone to improve it.

Just to be clear im going to reiterate the issue here:
How to load assets via HttpClient with a relative URL.

The reason this dosen't work out of the box is becasue when you making http request on the server, the server does not know what host your app is sitting on, it would be the same as making some random script that just does a http request.
When you do a relative http requests in a browser, the browser knows the host of the website it's sitting on and will prepend the host to the request.

The solution as mentioned above is to use a Interceptor (only works for HttpClient) which will prepend the host it any relative urls.

We are also working on a solution which will do this automatically, at a rough estimate i'd like to get this in f or v6.1

@Toxicable just to add, that if you are using the prerender api then, you need to load the full express server to be able to use the HttpInterceptor hack, because it won't work otherwise. Just like @OCCGU example.

Thx a lot @patrickmichalina, I followed your solution but I think I've simplified it a bit and also added a isPlatformServer to perform the magic only on the server side

The following code worked for me, just replace http://localhost:8000/ respectively the port number 8000 with the actual port number where your universal backend is running

import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';

import {isPlatformServer} from '@angular/common';

import {Observable} from 'rxjs'

@Injectable({
  providedIn: 'root'
})
export class HttpInterceptorService implements HttpInterceptor {

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (isPlatformServer(this.platformId) && req.url.includes('./')) {
      return next.handle(req.clone({
        url: `http://localhost:8000/${req.url.replace('./', '')}`
      }));
    }

    return next.handle(req);
  }
}

Can anybody speak to whether the HttpInterceptor option is still the best method as of Angular 7+?

Was this page helpful?
0 / 5 - 0 ratings