Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public enum ProcessedDataMetadataFields {
*/
ERROR("Error"),

/**
* a specific error code while processing the request, complements ERROR (optional)
*/
ERROR_CODE("Error-Code"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i think Error is a value from an enum ... so is specific

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

so if Error is a strict code -- I think eliminate this, just use Error; but if it's freeform message text, OK.


/**
* a warning code while processing the request
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,5 @@
package co.worklytics.psoxy.gateway.impl;

import java.io.IOException;
import java.net.ConnectException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.net.WWWFormCodec;
import org.apache.http.HttpStatus;
import org.apache.http.client.utils.URIBuilder;
import com.avaulta.gateway.pseudonyms.PseudonymImplementation;
import com.avaulta.gateway.pseudonyms.impl.UrlSafeTokenPseudonymEncoder;
import com.avaulta.gateway.tokens.ReversibleTokenizationStrategy;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.auth.Credentials;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import co.worklytics.psoxy.ControlHeader;
import co.worklytics.psoxy.ErrorCauses;
import co.worklytics.psoxy.ProcessedDataMetadataFields;
Expand All @@ -69,6 +13,7 @@
import co.worklytics.psoxy.gateway.HttpEventRequest;
import co.worklytics.psoxy.gateway.HttpEventResponse;
import co.worklytics.psoxy.gateway.ProcessedContent;
import co.worklytics.psoxy.gateway.ProxyConstants;
import co.worklytics.psoxy.gateway.SecretStore;
import co.worklytics.psoxy.gateway.SourceAuthStrategy;
import co.worklytics.psoxy.gateway.impl.output.OutputUtils;
Expand All @@ -81,6 +26,28 @@
import co.worklytics.psoxy.utils.ComposedHttpRequestInitializer;
import co.worklytics.psoxy.utils.GzipedContentHttpRequestInitializer;
import co.worklytics.psoxy.utils.URLUtils;
import com.avaulta.gateway.pseudonyms.PseudonymImplementation;
import com.avaulta.gateway.pseudonyms.impl.UrlSafeTokenPseudonymEncoder;
import com.avaulta.gateway.tokens.ReversibleTokenizationStrategy;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.auth.Credentials;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import dagger.Lazy;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -90,7 +57,32 @@
import lombok.SneakyThrows;
import lombok.Value;
import lombok.extern.java.Log;
import co.worklytics.psoxy.gateway.ProxyConstants;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.net.WWWFormCodec;
import org.apache.http.HttpStatus;
import org.apache.http.client.utils.URIBuilder;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@NoArgsConstructor(onConstructor_ = @Inject)
@Log
Expand Down Expand Up @@ -241,6 +233,17 @@ public HttpEventResponse handle(HttpEventRequest requestToProxy,
// our canonicalization code for this to go wrong
log.log(Level.WARNING, "Error parsing / building request URL", e);

// InvalidTokenException extends RuntimeException
if (e instanceof ReversibleTokenizationStrategy.InvalidTokenException ite) {
return HttpEventResponse.builder()
// should this be a 500? Maybe 422 Unprocessable Content?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

500 will make us retry, and same call would yield same results.
I think 422 makes more sense in this case and we can handle globally on Worklytics' side

.statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR)
.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
ErrorCauses.FAILED_TO_BUILD_URL.name())

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

This InvalidTokenException branch returns a 500 with FAILED_TO_BUILD_URL, but the same exception type is handled later as a client/token issue (TOKENIZED_REQUEST_PARAMETER_INVALID with a 409). That inconsistency will make it harder for callers to correctly react to token problems; consider mapping this branch to the same status + error cause as the later catch (or otherwise keep the semantics consistent across both paths).

Suggested change
// should this be a 500? Maybe 422 Unprocessable Content?
.statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR)
.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
ErrorCauses.FAILED_TO_BUILD_URL.name())
.statusCode(HttpStatus.SC_CONFLICT)
.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
ErrorCauses.TOKENIZED_REQUEST_PARAMETER_INVALID.name())

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah, I agree not a 500 ... whatever 4xx seems most semantically correct for the case.

.header(ProcessedDataMetadataFields.ERROR_CODE.getHttpHeader(), ite.getErrorCode().getCode())
.build();
}
Comment on lines +236 to +243

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

The newly added behavior that surfaces InvalidTokenException details during URL building (and the associated status/header/body mapping) doesn’t appear to be covered by tests. Please add/extend ApiDataRequestHandlerTest to assert the response status and X-Psoxy-Error header (and any new detail header/body) for this failure mode, so future refactors don’t silently change the contract.

Copilot uses AI. Check for mistakes.

return HttpEventResponse.builder()
.statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR)
.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
Expand Down Expand Up @@ -375,7 +378,7 @@ public HttpEventResponse handle(HttpEventRequest requestToProxy,
if (isSocketTimeoutException(e)) {
return buildNetworkTimeoutErrorResponse(builder, e);
}

builder.statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
builder.body("Failed to parse request; review logs");
builder.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
Expand All @@ -394,9 +397,12 @@ public HttpEventResponse handle(HttpEventRequest requestToProxy,
log.log(Level.WARNING, e.getMessage(), e);
return builder.build();
} catch (ReversibleTokenizationStrategy.InvalidTokenException e) {
// should this be 422 Unprocessable Content, rather than conflict?
builder.statusCode(HttpStatus.SC_CONFLICT);
builder.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
ErrorCauses.TOKENIZED_REQUEST_PARAMETER_INVALID.name());
builder.header(ProcessedDataMetadataFields.ERROR_CODE.getHttpHeader(), e.getErrorCode().getCode());

log.log(Level.WARNING, e.getMessage(), e);
return builder.build();
}
Expand Down Expand Up @@ -1045,7 +1051,7 @@ private boolean isSocketTimeoutException(Throwable throwable) {
*/
private HttpEventResponse buildNetworkTimeoutErrorResponse(
HttpEventResponse.HttpEventResponseBuilder builder, Throwable e) {

builder.statusCode(HttpStatus.SC_BAD_GATEWAY);
builder.body("Network timeout: unable to connect to target API. " +
"This could indicate: " +
Expand All @@ -1054,12 +1060,12 @@ private HttpEventResponse buildNetworkTimeoutErrorResponse(
"If using VPC connector, verify: VPC connector is active, CIDR range is correct, firewall allows egress, Cloud NAT is configured.");
builder.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
ErrorCauses.NETWORK_EGRESS_BLOCKED.name());

log.log(Level.SEVERE, "SocketTimeoutException: Network timeout connecting to target API", e);
log.log(Level.SEVERE, "Possible causes: " +
"1) Proxy network egress blocked - check VPC connector configuration, firewall rules, Cloud NAT; " +
"2) Target API unreachable or slow - check API status, DNS resolution");

return builder.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.avaulta.gateway.tokens;

import lombok.Getter;

import java.util.function.Function;

/**
Expand Down Expand Up @@ -40,12 +42,33 @@ default byte[] getReversibleToken(String originalDatum) {


/**
* Indicates that the token could not be reversed, likely because invalid
*
* Indicates that the token could not be reversed, likely because it is invalid.
*/
class InvalidTokenException extends RuntimeException {
public InvalidTokenException(String message, Throwable cause) {
super(message, cause);

@Getter
public enum ErrorCode {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

maybe not the best approach, but for certain cases (rules exceptions, this, and access errors) could be helpful to exactly know what's going on rather than a generic error

ALGORITHM_PARAMETER_ERROR("ITE01", "Failed to decrypt token; some algorithm parameter, such as iv, is wrong"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

no need to be so clever here; just return whole enum names ... don't care about a few extra chars in error case

BAD_PADDING("ITE02", "Failed to decrypt token; token appears to be corrupted or invalid, due to mismatch between token's padding and the padding expected by the cipher mode"),
Comment thread
eschultink marked this conversation as resolved.
Outdated
ILLEGAL_BLOCK_SIZE("ITE003", "Failed to decrypt token; token appears to be corrupted or invalid, as block size seems to differ from expected"),
DECRYPTION_FAILED("ITE004", "Failed to decrypt token; most likely because encryption key has been rotated");

private final String code;
private final String message;

ErrorCode(String code, String message) {
this.code = code;
this.message = message;
}

}

@Getter
private final ErrorCode errorCode;

public InvalidTokenException(ErrorCode errorCode, Throwable cause) {
super("[" + errorCode.getCode() + "] " + errorCode.getMessage(), cause);
this.errorCode = errorCode;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.avaulta.gateway.tokens.impl;

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.function.Function;
import com.avaulta.gateway.tokens.DeterministicTokenizationStrategy;
import com.avaulta.gateway.tokens.ReversibleTokenizationStrategy;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.Value;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
Expand All @@ -16,14 +18,13 @@
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import com.avaulta.gateway.tokens.DeterministicTokenizationStrategy;
import com.avaulta.gateway.tokens.ReversibleTokenizationStrategy;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.Value;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.function.Function;

@Builder
@RequiredArgsConstructor
Expand Down Expand Up @@ -130,13 +131,13 @@ public String getOriginalDatum(@NonNull byte[] reversibleToken) throws Reversibl
} catch (InvalidKeyException e) {
throw new RuntimeException("Invalid encryption key configuration", e);
} catch (InvalidAlgorithmParameterException e) {
throw new InvalidTokenException("Failed to decrypt token; some algorithm parameter, such as iv, is wrong", e);
throw new InvalidTokenException(InvalidTokenException.ErrorCode.ALGORITHM_PARAMETER_ERROR, e);
} catch (BadPaddingException e) {
throw new InvalidTokenException("Failed to decrypt token; token appears to be corrupted or invalid, due to mismatch between token's padding and the padding expected by the cipher mode", e);
throw new InvalidTokenException(InvalidTokenException.ErrorCode.BAD_PADDING, e);
} catch (IllegalBlockSizeException e) {
throw new InvalidTokenException("Failed to decrypt token; token appears to be corrupted or invalid, as block size seems to differ from expected", e);
throw new InvalidTokenException(InvalidTokenException.ErrorCode.ILLEGAL_BLOCK_SIZE, e);
} catch (RuntimeException e) {
throw new InvalidTokenException("Failed to decrypt token; most likely because encryption key has been rotated", e);
throw new InvalidTokenException(InvalidTokenException.ErrorCode.DECRYPTION_FAILED, e);
}
}

Expand Down
Loading