1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.server.internal.api;
18  
19  import static com.linecorp.centraldogma.server.internal.api.ContentServiceV1.IS_WATCH_REQUEST;
20  import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.newResponse;
21  
22  import java.util.Map;
23  import java.util.function.BiFunction;
24  
25  import com.google.common.collect.ImmutableMap;
26  
27  import com.linecorp.armeria.common.HttpResponse;
28  import com.linecorp.armeria.common.HttpStatus;
29  import com.linecorp.armeria.common.util.Exceptions;
30  import com.linecorp.armeria.server.HttpResponseException;
31  import com.linecorp.armeria.server.HttpStatusException;
32  import com.linecorp.armeria.server.ServerErrorHandler;
33  import com.linecorp.armeria.server.ServiceRequestContext;
34  import com.linecorp.armeria.server.annotation.ExceptionHandlerFunction;
35  import com.linecorp.centraldogma.common.AuthorizationException;
36  import com.linecorp.centraldogma.common.ChangeConflictException;
37  import com.linecorp.centraldogma.common.EntryNoContentException;
38  import com.linecorp.centraldogma.common.EntryNotFoundException;
39  import com.linecorp.centraldogma.common.InvalidPushException;
40  import com.linecorp.centraldogma.common.MirrorAccessException;
41  import com.linecorp.centraldogma.common.MirrorException;
42  import com.linecorp.centraldogma.common.PermissionException;
43  import com.linecorp.centraldogma.common.ProjectExistsException;
44  import com.linecorp.centraldogma.common.ProjectNotFoundException;
45  import com.linecorp.centraldogma.common.QueryExecutionException;
46  import com.linecorp.centraldogma.common.ReadOnlyException;
47  import com.linecorp.centraldogma.common.RedundantChangeException;
48  import com.linecorp.centraldogma.common.RepositoryExistsException;
49  import com.linecorp.centraldogma.common.RepositoryNotFoundException;
50  import com.linecorp.centraldogma.common.RevisionNotFoundException;
51  import com.linecorp.centraldogma.common.ShuttingDownException;
52  import com.linecorp.centraldogma.common.TextPatchConflictException;
53  import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
54  import com.linecorp.centraldogma.server.internal.storage.RequestAlreadyTimedOutException;
55  import com.linecorp.centraldogma.server.internal.storage.repository.RepositoryMetadataException;
56  import com.linecorp.centraldogma.server.metadata.MemberNotFoundException;
57  import com.linecorp.centraldogma.server.metadata.TokenNotFoundException;
58  
59  /**
60   * A default {@link ExceptionHandlerFunction} of HTTP API.
61   */
62  public final class HttpApiExceptionHandler implements ServerErrorHandler {
63  
64      /**
65       * A map of exception handler functions for well known exceptions.
66       */
67      private static final Map<Class<?>, BiFunction<ServiceRequestContext, Throwable, HttpResponse>>
68              exceptionHandlers;
69  
70      static {
71          final ImmutableMap.Builder<Class<?>,
72                  BiFunction<ServiceRequestContext, Throwable, HttpResponse>> builder = ImmutableMap.builder();
73  
74          builder.put(ChangeConflictException.class,
75                      (ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
76                                                  "The given changeset or revision has a conflict."))
77                 .put(EntryNotFoundException.class,
78                      (ctx, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
79                                                  "Entry '%s' does not exist.", cause.getMessage()))
80                 .put(EntryNoContentException.class,
81                      (ctx, cause) -> HttpResponse.of(HttpStatus.NO_CONTENT))
82                 .put(ProjectExistsException.class,
83                      (ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
84                                                  "Project '%s' exists already.", cause.getMessage()))
85                 .put(ProjectNotFoundException.class,
86                      (ctx, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
87                                                  "Project '%s' does not exist.", cause.getMessage()))
88                 .put(RedundantChangeException.class,
89                      (ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
90                                                  "The given changeset does not change anything."))
91                 .put(RepositoryExistsException.class,
92                      (ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
93                                                  "Repository '%s' exists already.", cause.getMessage()))
94                 .put(JsonPatchConflictException.class,
95                      (ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause))
96                 .put(TextPatchConflictException.class,
97                      (ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause))
98                 .put(RepositoryMetadataException.class,
99                      (ctx, cause) -> newResponse(ctx, HttpStatus.INTERNAL_SERVER_ERROR, cause))
100                .put(RepositoryNotFoundException.class,
101                     (ctx, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
102                                                 "Repository '%s' does not exist.", cause.getMessage()))
103                .put(RevisionNotFoundException.class,
104                     (ctx, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
105                                                 "Revision %s does not exist.", cause.getMessage()))
106                .put(TokenNotFoundException.class,
107                     (ctx, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause, cause.getMessage()))
108                .put(MemberNotFoundException.class,
109                     (ctx, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause, cause.getMessage()))
110                .put(QueryExecutionException.class,
111                     (ctx, cause) -> newResponse(ctx, HttpStatus.BAD_REQUEST, cause))
112                .put(UnsupportedOperationException.class,
113                     (ctx, cause) -> newResponse(ctx, HttpStatus.BAD_REQUEST, cause))
114                .put(InvalidPushException.class,
115                     (ctx, cause) -> newResponse(ctx, HttpStatus.BAD_REQUEST, cause))
116                .put(ReadOnlyException.class,
117                     (ctx, cause) -> newResponse(ctx, HttpStatus.SERVICE_UNAVAILABLE, cause))
118                .put(MirrorException.class,
119                     (ctx, cause) -> newResponse(ctx, HttpStatus.INTERNAL_SERVER_ERROR, cause))
120                .put(MirrorAccessException.class,
121                     (ctx, cause) -> newResponse(ctx, HttpStatus.FORBIDDEN, cause))
122                .put(AuthorizationException.class,
123                     (ctx, cause) -> newResponse(ctx, HttpStatus.UNAUTHORIZED, cause))
124                .put(PermissionException.class,
125                     (ctx, cause) -> newResponse(ctx, HttpStatus.FORBIDDEN, cause));
126 
127         exceptionHandlers = builder.build();
128     }
129 
130     @Override
131     public HttpResponse onServiceException(ServiceRequestContext ctx, Throwable cause) {
132         final Throwable peeledCause = Exceptions.peel(cause);
133 
134         if (peeledCause instanceof HttpStatusException ||
135             peeledCause instanceof HttpResponseException) {
136             return null;
137         }
138 
139         // Use precomputed map if the cause is instance of CentralDogmaException to access in a faster way.
140         final BiFunction<ServiceRequestContext, Throwable, HttpResponse> func =
141                 exceptionHandlers.get(peeledCause.getClass());
142         if (func != null) {
143             ctx.setShouldReportUnloggedExceptions(false);
144             return func.apply(ctx, peeledCause);
145         }
146 
147         if (peeledCause instanceof ShuttingDownException) {
148             if (Boolean.TRUE.equals(ctx.attr(IS_WATCH_REQUEST))) {
149                 ctx.setShouldReportUnloggedExceptions(false);
150                 // Use the same status code as handleWatchFailure() in ContentServiceV1.
151                 return HttpResponse.of(HttpStatus.NOT_MODIFIED);
152             }
153         }
154 
155         if (peeledCause instanceof IllegalArgumentException) {
156             ctx.setShouldReportUnloggedExceptions(false);
157             return newResponse(ctx, HttpStatus.BAD_REQUEST, peeledCause);
158         }
159 
160         if (peeledCause instanceof RequestAlreadyTimedOutException) {
161             ctx.setShouldReportUnloggedExceptions(false);
162             return newResponse(ctx, HttpStatus.SERVICE_UNAVAILABLE, peeledCause);
163         }
164 
165         return newResponse(ctx, HttpStatus.INTERNAL_SERVER_ERROR, peeledCause);
166     }
167 }