6

I'd like to unit test a service, however, when running the test, I get the following error:

Uncaught (in promise) SyntaxError: Unexpected token o in JSON at position 1 at MapSubscriber.project (auth.service.ts:217) at MapSubscriber.Array.concat.MapSubscriber._next (map.js:77) at MapSubscriber.Array.concat.Subscriber.next (Subscriber.js:89) at TakeSubscriber.Array.concat.TakeSubscriber._next (take.js:80) at TakeSubscriber.Array.concat.Subscriber.next (Subscriber.js:89) at ReplaySubject.Array.concat.ReplaySubject._subscribe (ReplaySubject.js:55) at ReplaySubject.Array.concat.Observable._trySubscribe (Observable.js:57) at ReplaySubject.Array.concat.Subject._trySubscribe (Subject.js:97) at ReplaySubject.Array.concat.Observable.subscribe (Observable.js:45) at TakeOperator.Array.concat.TakeOperator.call (take.js:60) at AnonymousSubject.Array.concat.Observable.subscribe (Observable.js:42) at MapOperator.Array.concat.MapOperator.call (map.js:54) at AnonymousSubject.Array.concat.Observable.subscribe (Observable.js:42) at CatchOperator.Array.concat.CatchOperator.call (catch.js:79) at AnonymousSubject.Array.concat.Observable.subscribe (Observable.js:42)

The corresponding line (auth.service.ts:217) is highlighted below in code. Running the application works perfectly fine, therefore I do not see an obvious reason for the test to fail.

NB: This SO post suggests that I'm parsing the object twice. But shouldn't it fail when running the application then, too?

auth.service.ts

public login(username: string, password: string): Observable<User> {
    // ...

    return this.http.request(path, requestOptions).map((response: Response) => {
        if (response.status === 200) {
          const token = response.json().token; // <<-- Uncaught (in promise) SyntaxError: Unexpected token o in JSON at position 1

          const user = this.extractUser(response);
          return user;
        }

        return null;
      })
      .catch(this.handleError);
  }

auth.service.spec.ts

describe('AuthService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        AuthService,
        MockBackend,
        BaseRequestOptions,
        {
          provide: Http,
          useFactory: (backend: MockBackend, options: BaseRequestOptions) => new Http(backend, options),
          deps: [MockBackend, BaseRequestOptions]
        }
      ],
      imports: [
        RouterTestingModule
      ],
    });
  });

  it('should return an Observable<User>', inject([AuthService, MockBackend], (authService: AuthService, mockBackend: MockBackend) => {
    mockBackend.connections.subscribe((connection: any) => {
      connection.mockRespond(new Response(new ResponseOptions({
        body: '{"token": "abc123", "name":"Jeff"}'
      })));
    });

    authService.login('jeff@example.com', 'password').subscribe(user => {
      expect(user.name).toEqual('Jeff');
    });
  }));

});

Logging the response outputs the following:

Response
  body: ReadableStream
    locked: true
    __proto__: Object
  bodyUsed: true
  headers: Headers
    __proto__: Headers
  ok: true
  redirected: false
  status: 200
  statusText: "OK"
  type: "default"
  url: ""
    __proto__: Response
Community
  • 1
  • 1
tilo
  • 14,009
  • 6
  • 68
  • 85

4 Answers4

19

The error Unexpected token ... in JSON at position 1 actually means that JSON.parse was applied to something that is not valid JSON - a random string or a value that is not a string at all which was coerced to string.

The message Unexpected token o ... implies that parsed value was most likely an object - which is coerced to [object ...] string.

The problem is clearly seen here:

Response
  body: ReadableStream
    locked: true
    __proto__: Object
  bodyUsed: true
  ...

Response object here is an instance of global Response constructor (a part of Fetch API), and the presence of ReadableStream clearly indicates this.

As it can be seen in Fetch polyfill, all that res.json() does is applying JSON.parse to some value

this.json = function() {
  return this.text().then(JSON.parse)
}

which is likely new ResponseOptions object that was erroneously supplied to global Response constructor, hence the error.

Angular has similarly named Response class which derives its interface from Fetch Response but obviously isn't compatible. The problem is that it was never imported and thus global Response was used instead.

How to fix

It should be

import { Response, ResponseOptions, ... } from '@angular/http';

How to prevent

Globals are declared in Typescript type definitions, they cannot be undeclared but can be re-declared in custom type definition:

custom.d.ts

declare var Headers: undefined;
declare var Request: undefined;
declare var Response: undefined;
declare var URLSearchParams: undefined;

Using globals instead of imported Http classes of the same name will result in type error:

TS2532: Object is possibly 'undefined'.

Which is a desirable behaviour as long as Fetch API isn't used in Angular app.

This is not an issue for HttpClient that replaced Http in Angular 4.3 and doesn't use classes of same names as the ones from Fetch API.

Community
  • 1
  • 1
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 1
    Great answer! IMHO, Angular team should consider using a different name. – maxisam May 22 '17 at 06:59
  • 1
    @maxisam It's hard to invent new names for basic things like headers and requests, but the fact that this Frankenstein API can be confused with Fetch API doesn't help. I came up with foolproof types after almost falling into the same trap. – Estus Flask Jun 17 '17 at 15:24
  • I would + this more than 1 if I could. Anyone have a surefire way to catch these pitfalls as part of a debugging scenario? – bdparrish Jul 09 '17 at 17:49
  • @bdparrish Do you have same case as above? Http providers could probably could be patched (i.e. extended to replace original providers) to check if their arguments are not instances of `window.Headers`, etc and throw if they are, but this looks like overkill to me. Doing this with type definitions like shown probably should the trick since it creates error output on compilation. – Estus Flask Jul 09 '17 at 23:23
  • I could have spent days walking through it in the debugger and never figured this out. HttpClient all the way. Thank you! – RMorrisey Oct 16 '17 at 18:47
2

SyntaxError: Unexpected token o in JSON at position 1

Reasons as per the error :

  • May be you are expecting Content-Type as JSON as a response from the API but you are getting response as a String start with alphabet o. Hence, It throws an error that Unexpected token o in JSON at position 1.
  • If the response header is text/html you need to parse, and if the response header is application/json it is already parsed for you.If you parse it again it will give you unexpected token o.

If response is already a JSON Object then no need to use response.json().

Debug Diva
  • 26,058
  • 13
  • 70
  • 123
0

No need to parse data.

Usually when you encounter this error, it means you do not need to parse your data.

Try using the 'token' directly.

For sanity, print your data in your console. (you can also Stringify it and print it see what it gives you)

mlk
  • 388
  • 2
  • 10
  • Thanks for your answer. However, I already covered both parts of your answer (no parsing, printing) in my question. – tilo Apr 28 '17 at 11:34
0

As it turned out I missed the import for Response:

Before: import { BaseRequestOptions, Http, ResponseOptions } from '@angular/http';

After: import { BaseRequestOptions, Http, Response, ResponseOptions } from '@angular/http';

Without importing Response from @angular/http, Response defaults to ECMAScript APIs.

tilo
  • 14,009
  • 6
  • 68
  • 85
  • Note: I'd like to appoint the reward to the one giving further details / explanation with respect to the background of this. – tilo Apr 28 '17 at 11:56
  • 1
    Yes, you missed it indeed. And the bad thing is that you missed the import when posting the question. It was the most relevant part, as it appears. – Estus Flask May 03 '17 at 03:07