--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.api;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * A simple DTO definitiong an <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15">HTTP Status Code</a>. Integer
+ * values used here are assigned through the
+ * <a href="https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml">IANA Status Code Registry</a>.
+ */
+@Beta
+@NonNullByDefault
+public final class HttpStatusCode {
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.3.1">200 OK</a>.
+ */
+ public static final HttpStatusCode OK = new HttpStatusCode(200, "OK");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.3.2">201 Created</a>.
+ */
+ public static final HttpStatusCode CREATED = new HttpStatusCode(201, "Created");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.3.3">202 Accepted</a>.
+ */
+ public static final HttpStatusCode ACCEPTED = new HttpStatusCode(202, "Accepted");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.3.4">203 Non-Authoritative Information</a>.
+ */
+ public static final HttpStatusCode NON_AUTHORITATIVE_INFORMATION =
+ new HttpStatusCode(203, "Non-Authoritative Information");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.3.5">204 No Content</a>.
+ */
+ public static final HttpStatusCode NO_CONTENT = new HttpStatusCode(204, "No Content");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.3.6">205 Reset Content</a>.
+ */
+ public static final HttpStatusCode RESET_CONTENT = new HttpStatusCode(205, "Reset Content");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.3.7">206 Partial Content</a>.
+ */
+ public static final HttpStatusCode PARTIAL_CONTENT = new HttpStatusCode(206, "Partial Content");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.4.1">300 Multiple Choices</a>.
+ */
+ public static final HttpStatusCode MULTIPLE_CHOICES = new HttpStatusCode(300, "Multiple Choices");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.4.2">301 Moved Permanently</a>.
+ */
+ public static final HttpStatusCode MOVED_PERMANENTLY = new HttpStatusCode(301, "Moved Permanently");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.4.3">302 Found</a>.
+ */
+ public static final HttpStatusCode FOUND = new HttpStatusCode(302, "Found");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.4.4">303 See Other</a>.
+ */
+ public static final HttpStatusCode SEE_OTHER = new HttpStatusCode(303, "See Other");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.4.5">304 Not Modified</a>.
+ */
+ public static final HttpStatusCode NOT_MODIFIED = new HttpStatusCode(304, "Not Modified");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.4.6">305 Use Proxy</a>.
+ */
+ public static final HttpStatusCode USE_PROXY = new HttpStatusCode(305, "Use Proxy");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.4.8">307 Temporary Redirect</a>.
+ */
+ public static final HttpStatusCode TEMPORARY_REDIRECT = new HttpStatusCode(307, "Temporary Redirect");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.4.9">308 Permanent Redirect</a>.
+ */
+ public static final HttpStatusCode PERMANENT_REDIRECT = new HttpStatusCode(308, "Permanent Redirect");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.1">400 Bad Request</a>.
+ */
+ public static final HttpStatusCode BAD_REQUEST = new HttpStatusCode(400, "Bad Request");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.2">401 Unauthorized</a>.
+ */
+ public static final HttpStatusCode UNAUTHORIZED = new HttpStatusCode(401, "Unauthorized");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.3">402 Payment Required</a>.
+ */
+ public static final HttpStatusCode PAYMENT_REQUIRED = new HttpStatusCode(402, "Payment Required");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.4">403 Forbidden</a>.
+ */
+ public static final HttpStatusCode FORBIDDEN = new HttpStatusCode(403, "Forbidden");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.5">404 Not Found</a>.
+ */
+ public static final HttpStatusCode NOT_FOUND = new HttpStatusCode(404, "Not Found");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.6">405 Method Not Allowed</a>.
+ */
+ public static final HttpStatusCode METHOD_NOT_ALLOWED = new HttpStatusCode(405, "Method Not Allowed");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.7">406 Not Acceptable</a>.
+ */
+ public static final HttpStatusCode NOT_ACCEPTABLE = new HttpStatusCode(406, "Not Acceptable");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.8">407 Proxy Authentication Required</a>.
+ */
+ public static final HttpStatusCode PROXY_AUTHENTICATION_REQUIRED =
+ new HttpStatusCode(407, "Proxy Authentication Required");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.9">408 Request Timeout</a>.
+ */
+ public static final HttpStatusCode REQUEST_TIMEOUT = new HttpStatusCode(408, "Request Timeout");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.10">409 Conflict</a>.
+ */
+ public static final HttpStatusCode CONFLICT = new HttpStatusCode(409, "Conflict");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.11">410 Gone</a>.
+ */
+ public static final HttpStatusCode GONE = new HttpStatusCode(410, "Gone");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.12">411 Length Required</a>.
+ */
+ public static final HttpStatusCode LENGTH_REQUIRED = new HttpStatusCode(411, "Length Required");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.13">412 Precondition Failed</a>.
+ */
+ public static final HttpStatusCode PRECONDITION_FAILED = new HttpStatusCode(412, "Precondition Failed");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.14">413 Content Too Large</a>.
+ */
+ public static final HttpStatusCode CONTENT_TOO_LARGE = new HttpStatusCode(413, "Content Too Large");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.15">414 Content Too Long</a>.
+ */
+ public static final HttpStatusCode URI_TOO_LONG = new HttpStatusCode(414, "URI Too Long");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.16">415 Unsupported Media Type</a>.
+ */
+ public static final HttpStatusCode UNSUPPORTED_MEDIA_TYPE = new HttpStatusCode(415, "Unsupported Media Type");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.17">416 Requested Range Not Satisfiable</a>.
+ */
+ public static final HttpStatusCode REQUESTED_RANGE_NOT_SATISFIABLE =
+ new HttpStatusCode(416, "Requested Range Not Satisfiable");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.18">417 Expectation Failed</a>.
+ */
+ public static final HttpStatusCode EXPECTATION_FAILED = new HttpStatusCode(417, "Expectation Failed");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.19">418 (Unused)</a>.
+ */
+ @Deprecated(forRemoval = true)
+ public static final HttpStatusCode I_M_A_TEAPOT = new HttpStatusCode(418, "I'm a teapot");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.20">421 Misdirected Request</a>.
+ */
+ public static final HttpStatusCode MISDIRECTED_REQUEST = new HttpStatusCode(421, "Misdirected Request");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.21">422 Unprocessable Content</a>.
+ */
+ public static final HttpStatusCode UNPROCESSABLE_CONTENT = new HttpStatusCode(422, "Unprocessable Content");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.5.22">426 Upgrade Required</a>.
+ */
+ public static final HttpStatusCode UPGRADE_REQUIRED = new HttpStatusCode(426, "Upgrade Required");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc6585#section-3">428 Precondition Required</a>.
+ */
+ public static final HttpStatusCode PRECONDITION_REQUIRED = new HttpStatusCode(428, "Precondition Required");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc6585#section-4">429 Too Many Requests</a>.
+ */
+ public static final HttpStatusCode TOO_MANY_REQUESTS = new HttpStatusCode(429, "Too Many Requests");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc6585#section-5">431 Request Header Fields Too Large</a>.
+ */
+ public static final HttpStatusCode REQUEST_HEADER_FIELDS_TOO_LARGE =
+ new HttpStatusCode(431, "Request Header Fields Too Large");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.6.1">500 Internal Server Error</a>.
+ */
+ public static final HttpStatusCode INTERNAL_SERVER_ERROR = new HttpStatusCode(500, "Internal Server Error");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.6.2">501 Not Implemented</a>.
+ */
+ public static final HttpStatusCode NOT_IMPLEMENTED = new HttpStatusCode(501, "Not Implemented");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.6.3">502 Bad Gateway</a>.
+ */
+ public static final HttpStatusCode BAD_GATEWAY = new HttpStatusCode(502, "Bad Gateway");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.6.4">503 Service Unavailable</a>.
+ */
+ public static final HttpStatusCode SERVICE_UNAVAILABLE = new HttpStatusCode(503, "Service Unavailable");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.6.5">504 Gateway Timeout</a>.
+ */
+ public static final HttpStatusCode GATEWAY_TIMEOUT = new HttpStatusCode(504, "Gateway Timeout");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.6.6">505 HTTP Version Not Supported</a>.
+ */
+ public static final HttpStatusCode HTTP_VERSION_NOT_SUPPORTED =
+ new HttpStatusCode(505, "HTTP Version Not Supported");
+ /**
+ * <a href="https://www.rfc-editor.org/rfc/rfc6585#section-6">511 Network Authentication Required</a>.
+ */
+ public static final HttpStatusCode NETWORK_AUTHENTICATION_REQUIRED =
+ new HttpStatusCode(511, "Network Authentication Required");
+
+ private final int code;
+ private final String phrase;
+
+ public HttpStatusCode(final int code, final @Nullable String phrase) {
+ if (code < 100 || code > 599) {
+ throw new IllegalArgumentException("Invalid statusCode " + code);
+ }
+ this.code = code;
+ this.phrase = phrase;
+ }
+
+ /**
+ * Returns the HTTP status code, {@code 100-599}.
+ *
+ * @return the HTTP status code
+ */
+ public int code() {
+ return code;
+ }
+
+ /**
+ * Returns the phrase or {@code null}.
+ *
+ * @return the phrase or {@code null}
+ */
+ public @Nullable String phrase() {
+ return phrase;
+ }
+
+ @Override
+ public int hashCode() {
+ return code;
+ }
+
+ @Override
+ public boolean equals(final @Nullable Object obj) {
+ return obj == this || obj instanceof HttpStatusCode other && code == other.code;
+ }
+
+ @Override
+ public String toString() {
+ final var sb = new StringBuilder(HttpStatusCode.class.getSimpleName()).append('(').append(code);
+ if (phrase != null) {
+ sb.append(' ').append(phrase);
+ }
+ return sb.append(')').toString();
+ }
+}
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
-import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.api.FormatParameters;
+import org.opendaylight.restconf.api.HttpStatusCode;
import org.opendaylight.restconf.api.MediaTypes;
import org.opendaylight.restconf.api.QueryParameters;
import org.opendaylight.restconf.api.query.PrettyPrintParam;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.errors.RestconfError;
import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.nb.rfc8040.URLConstants;
-import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
import org.opendaylight.restconf.server.api.ConfigurationMetadata;
import org.opendaylight.restconf.server.api.CreateResourceResult;
private final @NonNull RestconfServer server;
private final @NonNull ServerRequest emptyRequest;
private final @NonNull PrettyPrintParam prettyPrint;
+ private final @NonNull ErrorTagMapping errorTagMapping;
- public JaxRsRestconf(final RestconfServer server, final PrettyPrintParam prettyPrint) {
+ public JaxRsRestconf(final RestconfServer server, final ErrorTagMapping errorTagMapping,
+ final PrettyPrintParam prettyPrint) {
this.server = requireNonNull(server);
+ this.errorTagMapping = requireNonNull(errorTagMapping);
this.prettyPrint = requireNonNull(prettyPrint);
emptyRequest = ServerRequest.of(QueryParameters.of(), prettyPrint);
+
+ LOG.info("RESTCONF data-missing condition is reported as HTTP status {}", switch (errorTagMapping) {
+ case ERRATA_5565 -> "404 (Errata 5565)";
+ case RFC8040 -> "409 (RFC8040)";
+ });
}
private @NonNull ServerRequest requestOf(final UriInfo uriInfo) {
}
}
- private static void completeDataYangPATCH(final RestconfFuture<DataYangPatchResult> future,
+ private void completeDataYangPATCH(final RestconfFuture<DataYangPatchResult> future,
final AsyncResponse ar) {
future.addCallback(new JaxRsRestconfCallback<>(ar) {
@Override
Response transform(final DataYangPatchResult result) {
- final var status = result.status();
- final var builder = Response.status(statusOf(status))
- .entity(new YangPatchStatusBody(status));
+ final var patchStatus = result.status();
+ final var statusCode = statusOf(patchStatus);
+ final var builder = Response.status(statusCode.code(), statusCode.phrase())
+ .entity(new YangPatchStatusBody(patchStatus));
fillConfigurationMetadata(builder, result);
return builder.build();
}
- private static Status statusOf(final PatchStatusContext result) {
+ private HttpStatusCode statusOf(final PatchStatusContext result) {
if (result.ok()) {
- return Status.OK;
+ return HttpStatusCode.OK;
}
final var globalErrors = result.globalErrors();
if (globalErrors != null && !globalErrors.isEmpty()) {
}
}
}
- return Status.INTERNAL_SERVER_ERROR;
+ return HttpStatusCode.INTERNAL_SERVER_ERROR;
}
- private static Status statusOfFirst(final List<RestconfError> error) {
- return ErrorTags.statusOf(error.get(0).getErrorTag());
+ private @NonNull HttpStatusCode statusOfFirst(final List<RestconfError> error) {
+ return errorTagMapping.statusOf(error.get(0).getErrorTag());
}
});
}
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableMap;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.restconf.api.HttpStatusCode;
+import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+
+/**
+ * Mapping of {@link ErrorTag}s to {@link HttpStatusCode}s.
+ */
+@NonNullByDefault
+public enum ErrorTagMapping {
+ /**
+ * Mapping specified by <a href="https://www.rfc-editor.org/rfc/rfc8040#page-74">RFC8040</a>:
+ * {@link ErrorTag#DATA_MISSING} is reported as {@code 409 Conflict}. This may be confusing to users, as {@code GET}
+ * requests to non-existent datastore resources do not report {@code 404 Not Found} as would be expected from any
+ * other HTTP server.
+ */
+ RFC8040(HttpStatusCode.CONFLICT),
+ /**
+ * Mapping proposed by <a href="https://www.rfc-editor.org/errata/eid5565">Errata 5565</a>:
+ * {@link ErrorTag#DATA_MISSING} is reported as {@code 404 Not Found}. This is more in-line with expectations rooted
+ * in <a href="https://www.rfc-editor.org/rfc/rfc7231#section-6.5.4">HTTP/1.1 specification</a>.
+ */
+ ERRATA_5565(HttpStatusCode.NOT_FOUND);
+
+ private ImmutableMap<ErrorTag, HttpStatusCode> tagToStatus;
+
+ ErrorTagMapping(final HttpStatusCode dataMissing) {
+ tagToStatus = ImmutableMap.<ErrorTag, HttpStatusCode>builder()
+ .put(ErrorTag.IN_USE, HttpStatusCode.CONFLICT)
+ .put(ErrorTag.INVALID_VALUE, HttpStatusCode.BAD_REQUEST)
+ .put(ErrorTag.TOO_BIG, HttpStatusCode.CONTENT_TOO_LARGE)
+ .put(ErrorTag.MISSING_ATTRIBUTE, HttpStatusCode.BAD_REQUEST)
+ .put(ErrorTag.BAD_ATTRIBUTE, HttpStatusCode.BAD_REQUEST)
+ .put(ErrorTag.UNKNOWN_ATTRIBUTE, HttpStatusCode.BAD_REQUEST)
+ .put(ErrorTag.MISSING_ELEMENT, HttpStatusCode.BAD_REQUEST)
+ .put(ErrorTag.BAD_ELEMENT, HttpStatusCode.BAD_REQUEST)
+ .put(ErrorTag.UNKNOWN_ELEMENT, HttpStatusCode.BAD_REQUEST)
+ .put(ErrorTag.UNKNOWN_NAMESPACE, HttpStatusCode.BAD_REQUEST)
+
+ .put(ErrorTag.ACCESS_DENIED, HttpStatusCode.FORBIDDEN)
+ .put(ErrorTag.LOCK_DENIED, HttpStatusCode.CONFLICT)
+ .put(ErrorTag.RESOURCE_DENIED, HttpStatusCode.CONFLICT)
+ .put(ErrorTag.ROLLBACK_FAILED, HttpStatusCode.INTERNAL_SERVER_ERROR)
+ .put(ErrorTag.DATA_EXISTS, HttpStatusCode.CONFLICT)
+ .put(ErrorTag.DATA_MISSING, dataMissing)
+
+ .put(ErrorTag.OPERATION_NOT_SUPPORTED, HttpStatusCode.NOT_IMPLEMENTED)
+ .put(ErrorTag.OPERATION_FAILED, HttpStatusCode.INTERNAL_SERVER_ERROR)
+ .put(ErrorTag.PARTIAL_OPERATION, HttpStatusCode.INTERNAL_SERVER_ERROR)
+ .put(ErrorTag.MALFORMED_MESSAGE, HttpStatusCode.BAD_REQUEST)
+ .put(ErrorTags.RESOURCE_DENIED_TRANSPORT, HttpStatusCode.SERVICE_UNAVAILABLE)
+ .build();
+ }
+
+ /**
+ * Return the HTTP {@link HttpStatusCode} corresponding to specified {@link ErrorTag}.
+ *
+ * @param tag Error tag to map
+ * @return A {@link HttpStatusCode}
+ * @throws NullPointerException if {@code tag} is null
+ */
+ public HttpStatusCode statusOf(final ErrorTag tag) {
+ final var known = tagToStatus.get(requireNonNull(tag));
+ return known != null ? known : HttpStatusCode.INTERNAL_SERVER_ERROR;
+ }
+}
@Override
public Set<Object> getSingletons() {
+ final var errorTagMapping = servletFactory.errorTagMapping();
+
return Set.of(
new JsonJaxRsFormattableBodyWriter(), new XmlJaxRsFormattableBodyWriter(),
- new RestconfDocumentedExceptionMapper(databindProvider),
- new JaxRsRestconf(server, servletFactory.prettyPrint()));
+ new RestconfDocumentedExceptionMapper(databindProvider, errorTagMapping),
+ new JaxRsRestconf(server, errorTagMapping, servletFactory.prettyPrint()));
}
}).build())
.asyncSupported(true)
name = "default pretty-print",
description = "Control the default value of the '" + PrettyPrintParam.uriName + "' query parameter.")
boolean pretty$_$print() default false;
+
+ @AttributeDefinition(
+ name = "Report 404 on data-missing",
+ description = """
+ Control the HTTP status code reporting of conditions corresponding to "data-missing". When this is set
+ to true, the server will violate RFC8040 and report "404" instead of "409".
+
+ For details and reasoning see https://www.rfc-editor.org/errata/eid5565 and
+ https://mailarchive.ietf.org/arch/browse/netconf/?gbt=1&index=XcF9r3ek3LvZ4DjF-7_B8kxuiwA""")
+ boolean data$_$missing$_$is$_$404() default false;
}
private static final Logger LOG = LoggerFactory.getLogger(OSGiNorthbound.class);
registry = registryFactory.newInstance(FrameworkUtil.asDictionary(MdsalRestconfStreamRegistry.props(useSSE)));
servletProps = DefaultRestconfStreamServletFactory.props(configuration.restconf(), registry.getInstance(),
+ configuration.data$_$missing$_$is$_$404() ? ErrorTagMapping.ERRATA_5565 : ErrorTagMapping.RFC8040,
PrettyPrintParam.of(configuration.pretty$_$print()), useSSE,
new StreamsConfiguration(configuration.maximum$_$fragment$_$length(),
configuration.idle$_$timeout(), configuration.heartbeat$_$interval()),
LOG.debug("ListenersBroker restarted with {}", newUseSSE ? "SSE" : "Websockets");
}
final var newServletProps = DefaultRestconfStreamServletFactory.props(configuration.restconf(),
- registry.getInstance(), PrettyPrintParam.of(configuration.pretty$_$print()), useSSE,
+ registry.getInstance(),
+ configuration.data$_$missing$_$is$_$404() ? ErrorTagMapping.ERRATA_5565 : ErrorTagMapping.RFC8040,
+ PrettyPrintParam.of(configuration.pretty$_$print()), useSSE,
new StreamsConfiguration(configuration.maximum$_$fragment$_$length(),
configuration.idle$_$timeout(), configuration.heartbeat$_$interval()),
configuration.ping$_$executor$_$name$_$prefix(), configuration.max$_$thread$_$count());
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
+import org.opendaylight.restconf.api.HttpStatusCode;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.nb.jaxrs.JaxRsMediaTypes;
-import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.server.api.DatabindContext;
import org.opendaylight.restconf.server.spi.DatabindProvider;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.errors.Errors;
public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_TYPE;
- private static final Status DEFAULT_STATUS_CODE = Status.INTERNAL_SERVER_ERROR;
private static final QName ERROR_TYPE_QNAME = qnameOf("error-type");
private static final QName ERROR_TAG_QNAME = qnameOf("error-tag");
private static final QName ERROR_APP_TAG_QNAME = qnameOf("error-app-tag");
}
private final DatabindProvider databindProvider;
+ private final ErrorTagMapping errorTagMapping;
@Context
private HttpHeaders headers;
*
* @param databindProvider A {@link DatabindProvider}
*/
- public RestconfDocumentedExceptionMapper(final DatabindProvider databindProvider) {
+ public RestconfDocumentedExceptionMapper(final DatabindProvider databindProvider,
+ final ErrorTagMapping errorTagMapping) {
this.databindProvider = requireNonNull(databindProvider);
+ this.errorTagMapping = requireNonNull(errorTagMapping);
}
@Override
+ "we don't to have full stack trace - getMessage(..) method provides finer output.")
public Response toResponse(final RestconfDocumentedException exception) {
LOG.debug("Starting to map received exception to error response: {}", exception.getMessage());
- final Status responseStatus = getResponseStatusCode(exception);
- if (responseStatus != Response.Status.FORBIDDEN
- && responseStatus.getFamily() == Response.Status.Family.CLIENT_ERROR
- && exception.getErrors().isEmpty()) {
- // There should be at least one error entry for 4xx errors except 409 according to RFC8040, but we do not
- // have it. Issue a warning with the call trace so we can fix whoever was the originator.
- LOG.warn("Input exception has a family of 4xx but does not contain any descriptive errors", exception);
- }
+ final var responseStatus = getResponseStatusCode(exception);
final String serializedResponseBody;
final MediaType responseMediaType = transformToResponseMediaType(getSupportedMediaType());
serializedResponseBody = serializeExceptionToXml(exception, databindProvider);
}
- final Response preparedResponse = Response.status(responseStatus)
+ final Response preparedResponse = Response.status(responseStatus.code(), responseStatus.phrase())
.type(responseMediaType)
.entity(serializedResponseBody)
.build();
* Deriving of the status code from the thrown exception. At the first step, status code is tried to be read using
* {@link RestconfDocumentedException#getStatus()}. If it is {@code null}, status code will be derived from status
* codes appended to error entries (the first that will be found). If there are not any error entries,
- * {@link RestconfDocumentedExceptionMapper#DEFAULT_STATUS_CODE} will be used.
+ * {@link HttpStatusCode#INTERNAL_SERVER_ERROR} will be used.
*
* @param exception Thrown exception.
* @return Derived status code.
*/
- private static Status getResponseStatusCode(final RestconfDocumentedException exception) {
+ private HttpStatusCode getResponseStatusCode(final RestconfDocumentedException exception) {
final var errors = exception.getErrors();
if (errors.isEmpty()) {
// if the module, that thrown exception, doesn't specify status code, it is treated as internal
// server error
- return DEFAULT_STATUS_CODE;
+ return HttpStatusCode.INTERNAL_SERVER_ERROR;
}
final var allStatusCodesOfErrorEntries = errors.stream()
- .map(restconfError -> ErrorTags.statusOf(restconfError.getErrorTag()))
+ .map(restconfError -> errorTagMapping.statusOf(restconfError.getErrorTag()))
// we would like to preserve iteration order in collected entries - hence usage of LinkedHashSet
.collect(Collectors.toCollection(LinkedHashSet::new));
// choosing of the first status code from appended errors, if there are different status codes in error
*/
package org.opendaylight.restconf.nb.rfc8040.legacy;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.collect.ImmutableMap;
import javax.ws.rs.core.Response.Status;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.opendaylight.yangtools.yang.common.ErrorTag;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
- * {@link ErrorTag} mapping to HTTP errors. Aside from the mappings defined by
- * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-7">RFC8040 section 7</a>, we also define tags which
- * map to useful {@link Status} codes.
+ * Additional {@link ErrorTag}s.
*/
@NonNullByDefault
public final class ErrorTags {
// FIXME: redefine as SERVICE_UNAVAILABLE? It would be more obvious
public static final ErrorTag RESOURCE_DENIED_TRANSPORT = new ErrorTag("resource-denied-transport");
- private static final Logger LOG = LoggerFactory.getLogger(ErrorTags.class);
- private static final ImmutableMap<ErrorTag, Status> WELL_KNOWN_ERROR_TAGS = ImmutableMap.<ErrorTag, Status>builder()
- .put(ErrorTag.IN_USE, Status.CONFLICT)
- .put(ErrorTag.INVALID_VALUE, Status.BAD_REQUEST)
- .put(ErrorTag.TOO_BIG, Status.REQUEST_ENTITY_TOO_LARGE)
- .put(ErrorTag.MISSING_ATTRIBUTE, Status.BAD_REQUEST)
- .put(ErrorTag.BAD_ATTRIBUTE, Status.BAD_REQUEST)
- .put(ErrorTag.UNKNOWN_ATTRIBUTE, Status.BAD_REQUEST)
- .put(ErrorTag.MISSING_ELEMENT, Status.BAD_REQUEST)
- .put(ErrorTag.BAD_ELEMENT, Status.BAD_REQUEST)
- .put(ErrorTag.UNKNOWN_ELEMENT, Status.BAD_REQUEST)
- .put(ErrorTag.UNKNOWN_NAMESPACE, Status.BAD_REQUEST)
-
- .put(ErrorTag.ACCESS_DENIED, Status.FORBIDDEN)
- .put(ErrorTag.LOCK_DENIED, Status.CONFLICT)
- .put(ErrorTag.RESOURCE_DENIED, Status.CONFLICT)
- .put(ErrorTag.ROLLBACK_FAILED, Status.INTERNAL_SERVER_ERROR)
- .put(ErrorTag.DATA_EXISTS, Status.CONFLICT)
- .put(ErrorTag.DATA_MISSING, dataMissingHttpStatus())
-
- .put(ErrorTag.OPERATION_NOT_SUPPORTED, Status.NOT_IMPLEMENTED)
- .put(ErrorTag.OPERATION_FAILED, Status.INTERNAL_SERVER_ERROR)
- .put(ErrorTag.PARTIAL_OPERATION, Status.INTERNAL_SERVER_ERROR)
- .put(ErrorTag.MALFORMED_MESSAGE, Status.BAD_REQUEST)
- .put(ErrorTags.RESOURCE_DENIED_TRANSPORT, Status.SERVICE_UNAVAILABLE)
- .build();
-
private ErrorTags() {
// Hidden on purpose
}
-
- /**
- * Return the HTTP {@link Status} corresponding to specified {@link ErrorTag}.
- *
- * @param tag Error tag to map
- * @return HTTP Status
- * @throws NullPointerException if {@code tag} is null
- */
- public static Status statusOf(final ErrorTag tag) {
- final var known = WELL_KNOWN_ERROR_TAGS.get(requireNonNull(tag));
- return known != null ? known : Status.INTERNAL_SERVER_ERROR;
- }
-
- private static Status dataMissingHttpStatus() {
- // Control over the HTTP status reported on "data-missing" conditions. This defaults to disabled,
- // HTTP status 409 as specified by RFC8040 (and all previous drafts). See the discussion in:
- // https://www.rfc-editor.org/errata/eid5565
- // https://mailarchive.ietf.org/arch/msg/netconf/hkVDdHK4xA74NgvXzWP0zObMiyY/
- final var propName = "org.opendaylight.restconf.eid5565";
- final var propValue = System.getProperty(propName, "disabled");
- switch (propValue) {
- case "enabled":
- // RFC7231 interpretation: 404 Not Found
- LOG.info("RESTCONF data-missing condition is reported as HTTP status 404 (Errata 5565)");
- return Status.NOT_FOUND;
- case "disabled":
- break;
- default:
- LOG.warn("Unhandled {} value \"{}\", assuming disabled", propName, propValue);
- }
-
- // RFC8040 specification: 409 Conflict
- return Status.CONFLICT;
- }
}
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.aaa.web.servlet.ServletSupport;
import org.opendaylight.restconf.api.query.PrettyPrintParam;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.server.spi.RestconfStream;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
private static final String PROP_STREAMS_CONFIGURATION = ".streamsConfiguration";
private static final String PROP_RESTCONF = ".restconf";
private static final String PROP_PRETTY_PRINT = ".prettyPrint";
+ private static final String PROP_ERROR_TAG_MAPPING = ".errorTagMapping";
private final @NonNull String restconf;
+ private final @NonNull ErrorTagMapping errorTagMapping;
private final @NonNull PrettyPrintParam prettyPrint;
private final RestconfStream.Registry streamRegistry;
private final ServletSupport servletSupport;
public DefaultRestconfStreamServletFactory(final ServletSupport servletSupport, final String restconf,
final RestconfStream.Registry streamRegistry, final StreamsConfiguration streamsConfiguration,
- final PrettyPrintParam prettyPrint, final String namePrefix, final int corePoolSize,
- final boolean useWebsockets) {
+ final ErrorTagMapping errorTagMapping, final PrettyPrintParam prettyPrint, final String namePrefix,
+ final int corePoolSize, final boolean useWebsockets) {
this.servletSupport = requireNonNull(servletSupport);
this.restconf = requireNonNull(restconf);
if (restconf.endsWith("/")) {
}
this.streamRegistry = requireNonNull(streamRegistry);
this.streamsConfiguration = requireNonNull(streamsConfiguration);
+ this.errorTagMapping = requireNonNull(errorTagMapping);
this.prettyPrint = requireNonNull(prettyPrint);
pingExecutor = new DefaultPingExecutor(namePrefix, corePoolSize);
this.useWebsockets = useWebsockets;
this(servletSupport, (String) props.get(PROP_RESTCONF),
(RestconfStream.Registry) props.get(PROP_STREAM_REGISTRY),
(StreamsConfiguration) props.get(PROP_STREAMS_CONFIGURATION),
+ (ErrorTagMapping) props.get(PROP_ERROR_TAG_MAPPING),
(PrettyPrintParam) props.get(PROP_PRETTY_PRINT),
(String) props.get(PROP_NAME_PREFIX), (int) requireNonNull(props.get(PROP_CORE_POOL_SIZE)),
(boolean) requireNonNull(props.get(PROP_USE_WEBSOCKETS)));
return prettyPrint;
}
+ @Override
+ public ErrorTagMapping errorTagMapping() {
+ return errorTagMapping;
+ }
+
@Override
@Deactivate
public void close() {
}
public static Map<String, ?> props(final String restconf, final RestconfStream.Registry streamRegistry,
- final PrettyPrintParam prettyPrint, final boolean useSSE, final StreamsConfiguration streamsConfiguration,
- final String namePrefix, final int corePoolSize) {
+ final ErrorTagMapping errorTagMapping, final PrettyPrintParam prettyPrint, final boolean useSSE,
+ final StreamsConfiguration streamsConfiguration, final String namePrefix, final int corePoolSize) {
return Map.of(
PROP_RESTCONF, restconf,
PROP_STREAM_REGISTRY, streamRegistry,
+ PROP_ERROR_TAG_MAPPING, errorTagMapping,
PROP_PRETTY_PRINT, prettyPrint,
PROP_USE_WEBSOCKETS, !useSSE,
PROP_STREAMS_CONFIGURATION, streamsConfiguration,
package org.opendaylight.restconf.nb.rfc8040.streams;
import javax.servlet.http.HttpServlet;
-import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.opendaylight.restconf.api.query.PrettyPrintParam;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.server.spi.RestconfStream;
/**
* @deprecated This interface exists only to support SSE/Websocket delivery. It will be removed when support for
* WebSockets is removed.
*/
+@NonNullByDefault
@Deprecated(since = "7.0.0", forRemoval = true)
public interface RestconfStreamServletFactory {
/**
*
* @return the value of {@code {+restconf}} macro
*/
- @NonNull String restconf();
+ String restconf();
- @NonNull HttpServlet newStreamServlet();
+ HttpServlet newStreamServlet();
- @NonNull PrettyPrintParam prettyPrint();
+ PrettyPrintParam prettyPrint();
+
+ ErrorTagMapping errorTagMapping();
}
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.errors.RestconfError;
import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
import org.opendaylight.restconf.server.mdsal.MdsalDatabindProvider;
import org.opendaylight.restconf.server.mdsal.MdsalRestconfServer;
restconf = new JaxRsRestconf(
new MdsalRestconfServer(new MdsalDatabindProvider(new FixedDOMSchemaService(modelContext())),
dataBroker, rpcService, actionService, mountPointService),
- PrettyPrintParam.FALSE);
+ ErrorTagMapping.RFC8040, PrettyPrintParam.FALSE);
}
EffectiveModelContext modelContext() {
import org.opendaylight.restconf.api.query.PrettyPrintParam;
import org.opendaylight.restconf.nb.rfc8040.AbstractInstanceIdentifierTest;
import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.server.mdsal.MdsalDatabindProvider;
import org.opendaylight.restconf.server.mdsal.MdsalRestconfServer;
import org.opendaylight.yangtools.yang.common.QName;
final var restconf = new JaxRsRestconf(
new MdsalRestconfServer(new MdsalDatabindProvider(new FixedDOMSchemaService(IID_SCHEMA)),
dataBroker, rpcService, actionService, mountPointService),
- PrettyPrintParam.FALSE);
+ ErrorTagMapping.RFC8040, PrettyPrintParam.FALSE);
doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
doReturn(true).when(asyncResponse).resume(captor.capture());
restconf.postDataJSON(ApiPath.parse("instance-identifier-module:cont/cont1/reset"),
final var restconf = new JaxRsRestconf(
new MdsalRestconfServer(new MdsalDatabindProvider(new FixedDOMSchemaService(IID_SCHEMA)),
dataBroker, rpcService, actionService, mountPointService),
- PrettyPrintParam.FALSE);
+ ErrorTagMapping.RFC8040, PrettyPrintParam.FALSE);
doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
final var apiPath = ApiPath.parse("instance-identifier-module:cont/cont1/reset");
import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.api.query.PrettyPrintParam;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
import org.opendaylight.restconf.server.mdsal.MdsalDatabindProvider;
import org.opendaylight.restconf.server.mdsal.MdsalRestconfServer;
new MdsalRestconfServer(new MdsalDatabindProvider(
new FixedDOMSchemaService(SCHEMA_CONTEXT_WITH_MOUNT_POINTS)), dataBroker, rpcService, actionService,
mountPointService),
- PrettyPrintParam.FALSE);
+ ErrorTagMapping.RFC8040, PrettyPrintParam.FALSE);
}
/**
import org.opendaylight.mdsal.dom.api.DOMSchemaService.YangTextSourceExtension;
import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
import org.opendaylight.restconf.api.query.PrettyPrintParam;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.server.mdsal.MdsalDatabindProvider;
import org.opendaylight.restconf.server.mdsal.MdsalRestconfServer;
import org.opendaylight.yangtools.yang.common.ErrorTag;
new MdsalRestconfServer(new MdsalDatabindProvider(
new FixedDOMSchemaService(() -> MODEL_CONTEXT, sourceProvider)), dataBroker, rpcService, actionService,
mountPointService),
- PrettyPrintParam.FALSE);
+ ErrorTagMapping.RFC8040, PrettyPrintParam.FALSE);
}
/**
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.restconf.nb.rfc8040.legacy;
+package org.opendaylight.restconf.nb.rfc8040;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class ErrorTagsTest {
@ParameterizedTest(name = "{0} => {1}")
@MethodSource
- void testStatusOf(final String tagName, final int status) {
- assertEquals(status, ErrorTags.statusOf(new ErrorTag(tagName)).getStatusCode());
+ void testStatusOf(final @NonNull String tagName, final int status) {
+ assertEquals(status, ErrorTagMapping.RFC8040.statusOf(new ErrorTag(tagName)).code());
}
static List<Arguments> testStatusOf() {
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.errors.RestconfError;
import org.opendaylight.restconf.nb.jaxrs.JaxRsMediaTypes;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
import org.opendaylight.restconf.server.api.DatabindContext;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
final var schemaContext = YangParserTestUtils.parseYangResources(
RestconfDocumentedExceptionMapperTest.class, "/restconf/impl/ietf-restconf@2017-01-26.yang",
"/instanceidentifier/yang/instance-identifier-patch-module.yang");
- exceptionMapper = new RestconfDocumentedExceptionMapper(() -> DatabindContext.ofModel(schemaContext));
+ exceptionMapper = new RestconfDocumentedExceptionMapper(() -> DatabindContext.ofModel(schemaContext),
+ ErrorTagMapping.RFC8040);
}
/**