Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: send 404 for findById(), if resource is not found #1617

Closed
wants to merge 1 commit into from

Conversation

hacksparrow
Copy link
Contributor

@hacksparrow hacksparrow commented Aug 17, 2018

For fixing #1472 and #1469.

Update: Since the causes of #1472 and #1469 are worlds apart. I will open a new PR for addressing #1472.

@hacksparrow hacksparrow self-assigned this Aug 17, 2018
@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch 2 times, most recently from 3401c63 to ede1827 Compare August 17, 2018 10:57
@raymondfeng
Copy link
Contributor

@hacksparrow Did you miss some files in the PR? I only see changes in tests.

@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch 2 times, most recently from 9c203fa to 151f25b Compare August 17, 2018 20:10
@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch from 151f25b to 35880f3 Compare August 20, 2018 14:39
@@ -35,7 +44,18 @@ export class TodoController {
@param.path.number('id') id: number,
@param.query.boolean('items') items?: boolean,
): Promise<Todo> {
return await this.todoRepo.findById(id);
try {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If legacy-juggler-bridge throws the specific error instead of the generic Error,
we won't need to try-catch here.

However, there is a catch: legacy-juggler-bridge needs to import @loopback/rest,
which would create a cyclic dependency.

Maybe HttpErrors should be a package of it's own?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one's for fixing #1469.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does juggler throw errors with status or statusCode set for http? If so, we can check that pattern instead of forcing HttpErrors.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@hacksparrow hacksparrow Aug 21, 2018

Choose a reason for hiding this comment

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

Juggler does not throw, it just returns null. I am exploring the possibilities at Juggler level, because in another case it throws with code and statusCode.

@@ -128,7 +128,7 @@ export class DefaultCrudRepository<T extends Entity, ID>
this.modelClass = dataSource.createModel<juggler.PersistedModelClass>(
definition.name,
properties,
Object.assign({strict: true}, definition.settings),
Object.assign({strict: true}, {strictDelete: true}, definition.settings),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This fixes #1472.

@hacksparrow
Copy link
Contributor Author

@raymondfeng pushed two implementation changes.

@hacksparrow hacksparrow changed the title [WIP] fix: fix improper response code [WIP] fix: fix Incorrect response codes for invalid HTTP calls #1469 Aug 20, 2018
@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch from 35880f3 to 3731d0f Compare August 20, 2018 15:03
@hacksparrow
Copy link
Contributor Author

Copy link
Contributor

@virkt25 virkt25 left a comment

Choose a reason for hiding this comment

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

How are the delete tests related to #1469? I understand the demonstration but I'm guessing the fix will live in legacy-juggler?

/*
This is just a quick hack to demonstrate the problem.
*/
if (e.message.match(/ound with id/)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

found

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just a quick hack to demonstrate the problem.

😃

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should for a solution which requires no change in controllers.

@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch 2 times, most recently from d8afd85 to a886344 Compare August 23, 2018 13:07
@hacksparrow
Copy link
Contributor Author

This PR should land for this to work.

@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch from a886344 to d43b48f Compare August 23, 2018 14:00
@bajtos
Copy link
Member

bajtos commented Aug 23, 2018

Cross-posting my comment from loopbackio/loopback-datasource-juggler#1620:

  • Juggler as the data-access layer should not be coupled with the specific requirements of transport layers like REST.
  • In LoopBack 1.x-3.x, we are able to convert null result to 404 error. I am sure we can do that in LB4 too.

A link to the solution we use in LoopBack 3.x: https://github.com/strongloop/loopback/blob/master/lib/persisted-model.js#L85-L95

In my opinion, the solution to this problem is to modify the generated CRUD Controller method findTodoById to convert null result into an HTTP error.

Using our TodoController as an example:

export class TodoController {
  // ...

  @get('/todos/{id}')
  async findTodoById(
    @param.path.number('id') id: number,
  ): Promise<Todo> {
    const result = await this.todoRepo.findById(id);
    if (result == null) {
      throw new HttpError(
        404, 
        `Todo #${id} was not found.`,
        {code: 'MODEL_NOT_FOUND'}
      );
    }
    return result;
  }
}

To avoid unnecessary repetition of the code handling "null to 404" conversion, we can extract a shared helper function into @loopback/rest package.

import {convertNullToNotFoundError} from '@loopback/rest';

export class TodoController {
  // ...

  @get('/todos/{id}')
  async findTodoById(
    @param.path.number('id') id: number,
  ): Promise<Todo> {
    const result = await this.todoRepo.findById(id);
    convertNullToNotFoundError(result);
    return result;
  }
}

Thoughts?

@hacksparrow
Copy link
Contributor Author

@virkt25 I have update the PR with only the relevant changes.

@bajtos bajtos changed the title [WIP] fix: fix Incorrect response codes for invalid HTTP calls #1469 [WIP] fix Incorrect response codes for invalid HTTP calls #1469 Aug 24, 2018
@hacksparrow
Copy link
Contributor Author

@bajtos lovely solution! I will go ahead with it.

@bajtos
Copy link
Member

bajtos commented Aug 24, 2018

@hacksparrow cool :) Maybe we can find a better name than convertNullToNotFoundError, perhaps throwNotFoundErrorWhenNull?

@hacksparrow hacksparrow changed the title [WIP] fix Incorrect response codes for invalid HTTP calls #1469 fix: send 404 for findById(), if resource is not found Aug 27, 2018
@@ -210,12 +210,13 @@ export class DefaultCrudRepository<T extends Entity, ID>
}

async findById(id: ID, filter?: Filter, options?: Options): Promise<T> {
options = options || {};
// WIP: introducing `strictFind` to return `404` for non-existent resources. Currenyly returns `null`
options.strictFind = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be configurable at the @model() level similar to strict and strictDelete?

@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch 2 times, most recently from 2f0ae48 to 8c5cf03 Compare August 28, 2018 15:49
@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch 2 times, most recently from 671c825 to 65a95d9 Compare August 28, 2018 16:34
send 404 for findById(), if resource is not found
@hacksparrow hacksparrow force-pushed the fix/improper-response-code branch from 65a95d9 to ad95035 Compare August 28, 2018 16:37
@hacksparrow
Copy link
Contributor Author

hacksparrow commented Aug 28, 2018

Here is what is happening in the issue this PR is targeted at:

legacy-juggler-bridge throws a generic error when findById() does not find the model. Since this generic error is not an HttpError, it is being interpreted as a 500 error and is sent in the response.

Making legacy-juggler-bridge throw a HttpErrors.NotFound fixes the problem. However, we want to keep repositories transport-agnostic and don't want the solution implemented at the repository level.

We could make Juggler throw HttpErrors.NotFound, but then we want to keep it also transport-agnostic.

This PR implements the solution at the controller level (to initiate the discussion). However, requiring to add the http-errors module, and try-catching in controller functions don't look elegant. I haven't confirmed yet, but all operations involving *byId() might be affected and we have to try-catch any controllers calling them. This will result in a messy-looking controller file.

So the ideal solution should be at the framework-level but unexposed or barely-exposed to the user.

Miroslav and I have been exploring various options but have not found a good solution yet. It would be great to hear from you all.

There are the possible starting points:

  1. legacy-juggler-bridge throws a generic error => We have the problem I have described above. This is the current state.
  2. legacy-juggler-bridge throws a HttpErrors.NotFound error => Repository is not transport-agnostic anymore.
  3. legacy-juggler-bridge returns null => This is basically resolving a promise with null. A resolved promise is expected to have something, not null. This unexpected behavior can have very undesirable consequences. Besides, this will require change at the EntityCrudRepository level.

With reference to legacy-juggler-bridge:

Why is the return type of findOne() Promise<T | null>, but the return type of findById() Promise<T>? If findOne() fails to find something, it resolves the promise with a null; if findById() fails to find something to rejects the promise. Isn't this inconsistent behavior? Should it be a reject it in both the cases?

@virkt25
Copy link
Contributor

virkt25 commented Aug 29, 2018

What does the error from juggler look like?

I don’t think we can just convert all errors from Juggler to a 404 because what if the juggler didn’t find the model because of a database connection issue?

My guess is that the reason findOne returns null is since it takes a filter for a search and returns the first item matching the criteria and nothing matching isn’t necessarily a NotFoundError but maybe it is?

I think as pointed out, we should aim for consistency and the findByID method is inconsistent and should return null if no item exists (now we know there wasn’t a database error). In the controller we can introduce a helper function to return a 404 for *ByID methods if the result is undefined — which can live in the rest package.

return returnModel(await this.repo.findByID(id))

returnModel(model) {
  if (!model) return HttpErrors.NotFound;
  retrun model;
}

Let’s see what others think before proceeding.

@hacksparrow
Copy link
Contributor Author

What does the error from juggler look like?

If the model is not found, it returns/resolves null; does not throw or reject promise for not-found.

@hacksparrow
Copy link
Contributor Author

hacksparrow commented Aug 29, 2018

In the controller we can introduce a helper function to return a 404 for *ByID methods if the result is undefined — which can live in the rest package.

This is exactly what we were trying for. But this requires change in EntityCrudRepository:

findById(id: ID, filter?: Filter, options?: Options): Promise<T>;

should become

findById(id: ID, filter?: Filter, options?: Options): Promise<T | null>;

To me, a model-querying promise resolving two different types seem illogical; and especially if it resolves null. You either find by resolving with a model, or don't find by rejecting the promise.

@hacksparrow
Copy link
Contributor Author

@raymondfeng any suggestions?

@b-admike
Copy link
Contributor

@hacksparrow After reading through this thread, I think the solutions suggested by @bajtos at #1617 (comment) and @virkt25 at #1617 (comment) are sensible. From the 3 starting points you listed, I don't think 2) and 3) are good approaches for the reasons you mentioned.

@raymondfeng
Copy link
Contributor

I'm leaning toward findById(...): Promise<T | null> for the juggler bridge and doing the 404 mapping inside the controller.

@bajtos
Copy link
Member

bajtos commented Aug 30, 2018

Another option to consider:

IMO, the proposed API findById(...): Promise<T | null> is not ergonomic, as it requires everybody to handle the "not found" case, even in code where we are fairly certain the instance will be found (e.g. test cases).

@bajtos
Copy link
Member

bajtos commented Aug 30, 2018

I went ahead and implemented my proposal in #1658.

@hacksparrow
Copy link
Contributor Author

Closing in favor of #1658.

@hacksparrow hacksparrow closed this Sep 3, 2018
@hacksparrow hacksparrow deleted the fix/improper-response-code branch November 8, 2018 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants