-
Notifications
You must be signed in to change notification settings - Fork 148
Emit errors through Search API #1000
base: master
Are you sure you want to change the base?
Changes from 9 commits
5f92509
3f99e4b
54918ef
39d4dfe
b2050c2
0ed8474
3c74029
072ffd2
5c5f93d
09599a1
b3df329
0073d1f
54a7071
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package handle | |
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"io/ioutil" | ||
"net/http" | ||
"time" | ||
|
@@ -47,6 +48,11 @@ type SearchResponse struct { | |
Users []User `json:"users,omitempty"` | ||
} | ||
|
||
// SearchError represents an error with the Search API request. | ||
type SearchError struct { | ||
Message string `json:"message"` | ||
} | ||
|
||
// ShortLink represents the short_link field of Search API respond. | ||
type ShortLink struct { | ||
Alias string `json:"alias,omitempty"` | ||
|
@@ -79,15 +85,15 @@ func Search( | |
defer r.Body.Close() | ||
Coteh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
i.SearchFailed(err) | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
emitSearchError(w, err) | ||
return | ||
} | ||
|
||
var body SearchRequest | ||
err = json.Unmarshal(buf, &body) | ||
if err != nil { | ||
i.SearchFailed(err) | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
emitSearchError(w, err) | ||
return | ||
} | ||
|
||
|
@@ -99,22 +105,22 @@ func Search( | |
filter, err := search.NewFilter(body.Filter.MaxResults, body.Filter.Resources, body.Filter.Orders) | ||
if err != nil { | ||
i.SearchFailed(err) | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
emitSearchError(w, err) | ||
return | ||
} | ||
|
||
results, err := searcher.Search(query, filter) | ||
if err != nil { | ||
i.SearchFailed(err) | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
emitSearchError(w, err) | ||
return | ||
} | ||
|
||
response := newSearchResponse(results) | ||
respBody, err := json.Marshal(&response) | ||
if err != nil { | ||
i.SearchFailed(err) | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
emitSearchError(w, err) | ||
return | ||
} | ||
|
||
Coteh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
@@ -168,7 +174,7 @@ func (f *Filter) resourcesString() []string { | |
return resources | ||
} | ||
|
||
func newSearchResponse(result search.Result) SearchResponse { | ||
func newSearchResponse(result search.ResourceResult) SearchResponse { | ||
shortLinks := make([]ShortLink, len(result.ShortLinks)) | ||
for i := 0; i < len(result.ShortLinks); i++ { | ||
shortLinks[i] = newShortLink(result.ShortLinks[i]) | ||
|
@@ -205,3 +211,24 @@ func newUser(user entity.User) User { | |
UpdatedAt: user.UpdatedAt, | ||
} | ||
} | ||
|
||
func emitSearchError(w http.ResponseWriter, err error) { | ||
var ( | ||
code = http.StatusInternalServerError | ||
u search.ErrUserNotProvided | ||
r search.ErrUnknownResource | ||
) | ||
if errors.As(err, &u) { | ||
code = http.StatusUnauthorized | ||
} | ||
if errors.As(err, &r) { | ||
code = http.StatusNotFound | ||
} | ||
errResp, err := json.Marshal(SearchError{ | ||
Message: err.Error(), | ||
}) | ||
if err != nil { | ||
return | ||
} | ||
http.Error(w, string(errResp), code) | ||
} | ||
Comment on lines
+215
to
+234
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each search error is different in the code. I recommend putting the content of this function back to the code so that the reader can follow the error handling logic. This is an example when abstraction is reducing readability. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
package search | ||
|
||
import ( | ||
"errors" | ||
"strings" | ||
"time" | ||
|
||
|
@@ -22,12 +21,32 @@ type Search struct { | |
|
||
// Result represents the result of a search query. | ||
type Result struct { | ||
Resources ResourceResult | ||
Err error | ||
} | ||
|
||
// ResourceResult represents the resources obtained from a search query. | ||
type ResourceResult struct { | ||
ShortLinks []entity.ShortLink | ||
Users []entity.User | ||
} | ||
|
||
// ErrUnknownResource represents unknown search resource error. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can move this new error.go file. Wdty? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In other usecase files, such as creator.go the error types are embedded within the file. I am not sure what the benefit of having it in a separate file would be, especially since there's only two custom error types at the moment. |
||
type ErrUnknownResource struct{} | ||
|
||
func (e ErrUnknownResource) Error() string { | ||
return "unknown resource" | ||
} | ||
|
||
// ErrUserNotProvided represents user not provided for search query. | ||
type ErrUserNotProvided struct{} | ||
|
||
func (e ErrUserNotProvided) Error() string { | ||
return "user not provided" | ||
} | ||
|
||
// Search finds resources based on specified criteria. | ||
func (s Search) Search(query Query, filter Filter) (Result, error) { | ||
func (s Search) Search(query Query, filter Filter) (ResourceResult, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are already returning error here. I wonder why are we putting extract error in |
||
resultCh := make(chan Result) | ||
defer close(resultCh) | ||
|
||
|
@@ -38,50 +57,61 @@ func (s Search) Search(query Query, filter Filter) (Result, error) { | |
go func() { | ||
result, err := s.searchResource(filter.resources[i], orders[i], query, filter) | ||
if err != nil { | ||
// TODO(issue#865): Handle errors of Search API | ||
s.logger.Error(err) | ||
resultCh <- Result{} | ||
resultCh <- Result{ | ||
Resources: ResourceResult{}, | ||
Err: err, | ||
} | ||
Coteh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return | ||
} | ||
resultCh <- result | ||
resultCh <- Result{ | ||
Resources: result, | ||
Err: nil, | ||
} | ||
}() | ||
} | ||
|
||
timeout := time.After(s.timeout) | ||
var results []Result | ||
var results []ResourceResult | ||
var resultErr error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not needed based on the previous comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is still needed, because we would like to return only the first error that occurred. What do you think? |
||
for i := 0; i < len(filter.resources); i++ { | ||
select { | ||
case result := <-resultCh: | ||
results = append(results, result) | ||
// Only return the first error encountered | ||
if resultErr == nil { | ||
resultErr = result.Err | ||
} | ||
results = append(results, result.Resources) | ||
case <-timeout: | ||
return mergeResults(results), nil | ||
} | ||
} | ||
|
||
return mergeResults(results), nil | ||
return mergeResults(results), resultErr | ||
} | ||
|
||
func (s Search) searchResource(resource Resource, orderBy order.Order, query Query, filter Filter) (Result, error) { | ||
func (s Search) searchResource(resource Resource, orderBy order.Order, query Query, filter Filter) (ResourceResult, error) { | ||
switch resource { | ||
case ShortLink: | ||
return s.searchShortLink(query, orderBy, filter) | ||
case User: | ||
return s.searchUser(query, orderBy, filter) | ||
default: | ||
return Result{}, errors.New("unknown resource") | ||
return ResourceResult{}, ErrUnknownResource{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's revert this. Didn't seems to provide value here. |
||
} | ||
} | ||
|
||
// TODO(issue#866): Simplify searchShortLink function | ||
func (s Search) searchShortLink(query Query, orderBy order.Order, filter Filter) (Result, error) { | ||
func (s Search) searchShortLink(query Query, orderBy order.Order, filter Filter) (ResourceResult, error) { | ||
if query.User == nil { | ||
s.logger.Error(errors.New("user not provided")) | ||
return Result{}, nil | ||
err := ErrUserNotProvided{} | ||
s.logger.Error(err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's directly return error here and log it at the caller. |
||
return ResourceResult{}, err | ||
} | ||
|
||
shortLinks, err := s.getShortLinkByUser(*query.User) | ||
if err != nil { | ||
return Result{}, err | ||
return ResourceResult{}, err | ||
} | ||
|
||
var matchedAliasByAll, matchedAliasByAny, matchedLongLinkByAll, matchedLongLinkByAny []entity.ShortLink | ||
|
@@ -115,14 +145,14 @@ func (s Search) searchShortLink(query Query, orderBy order.Order, filter Filter) | |
|
||
filteredShortLinks := filterShortLinks(mergedShortLinks, filter) | ||
|
||
return Result{ | ||
return ResourceResult{ | ||
ShortLinks: filteredShortLinks, | ||
Users: nil, | ||
}, nil | ||
} | ||
|
||
func (s Search) searchUser(query Query, orderBy order.Order, filter Filter) (Result, error) { | ||
return Result{}, nil | ||
func (s Search) searchUser(query Query, orderBy order.Order, filter Filter) (ResourceResult, error) { | ||
return ResourceResult{}, nil | ||
} | ||
|
||
func (s Search) getShortLinkByUser(user entity.User) ([]entity.ShortLink, error) { | ||
|
@@ -171,8 +201,8 @@ func toOrders(ordersBy []order.By) []order.Order { | |
return orders | ||
} | ||
|
||
func mergeResults(results []Result) Result { | ||
var mergedResult Result | ||
func mergeResults(results []ResourceResult) ResourceResult { | ||
var mergedResult ResourceResult | ||
|
||
for _, result := range results { | ||
mergedResult.ShortLinks = append(mergedResult.ShortLinks, result.ShortLinks...) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder what is this for? Are we going handle it in the frontend?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will provide information about the search error that occurred. So far, we have "user not provided" and "unknown resource" errors. I think it would make sense to emit these errors in the frontend, even though they may only show up in rare circumstances (e.g. user somehow gets logged out right before they search for something). What do you think?