Here is my ever-growing validationRejectionHandler for my Spray project:
implicit def validationRejectionHandler = RejectionHandler {
case ValidationRejection(errMsg,_) :: _ =>
logger.info(s"received validation error: $errMsg")
complete(StatusCodes.Unauthorized,errMsg)
case MalformedQueryParamRejection(parameterName, errorMsg, cause) :: _ =>
logger.debug(s"received MalformedQueryParamRejection error: $errorMsg")
complete(BadRequest -> GenericMessageObj(s"The query parameter $parameterName was malformed: $errorMsg"))
case spray.routing.AuthorizationFailedRejection :: _ =>
//todo - this string shouldn't be here
logger.info("received authentication error")
complete(StatusCodes.Unauthorized, "User is not authorized to this resource")
case MalformedRequestContentRejection(msg, causeOpt) :: _ =>
complete {
causeOpt.map { cause =>
cause match {
case e: InvalidFormatException =>
val fieldNameMatcher = """\["(.+)"\]""".r.unanchored
val fieldTypeMatcher = """(\w+)$""".r.unanchored
e.getPath.toString match {
case fieldNameMatcher(fieldName) =>
e.getTargetType.toString match {
case fieldTypeMatcher(fieldType) =>
val fieldTypeLowerCase = fieldType.toLowerCase()
BadRequest -> GenericMessageObj(s"""Invalid data: "${fieldName}" must be a ${fieldTypeLowerCase} value, but received ${e.getValue}""")
case _ =>
BadRequest -> GenericMessageObj(s"""${e.getValue} is an improper type for field "${fieldName}""")
}
case _ =>
logger.debug(s"Failed pattern match: ${e.getPath.toString}")
BadRequest -> GenericMessageObj("Invalid payload format")
}
case e: UnrecognizedPropertyException => BadRequest -> GenericMessageObj(s"Unrecognized property: ${e.getPropertyName}")
case e: JsonMappingException =>
if(cause.getCause == null || cause.getCause.getMessage == null){
val deserializationMsgMatcher = """Can not deserialize instance of scala\.collection\.(Seq) out of (VALUE_NUMBER_INT) [\s\S]+"(name)":[+-]?\d\};[\s\S]+\["(\3)"\].+""".r.unanchored
cause.getMessage match {
case deserializationMsgMatcher(expected, actual, fieldName, _) =>
logger.debug(s"Desrializaiton error at $fieldName: Found $actual instead of $expected")
BadRequest -> GenericMessageObj(s"Invalid format for $fieldName")
case _ =>
BadRequest -> GenericMessageObj(s"${cause.getMessage}")
}
} else if (!cause.getCause.getMessage.isEmpty) {
BadRequest -> GenericMessageObj(cause.getCause.getMessage)
} else {
BadRequest -> GenericMessageObj(s"Invalid data format")
}
case _ => BadRequest -> GenericMessageObj(s"An unknown error occurred.")
}
}
}
case spray.routing.MissingHeaderRejection(headerName) :: _ =>
complete(BadRequest -> GenericMessageObj("%s header is missing.".format(headerName)))
}
}
It seems crazy to me that this convoluted bit of code - complete with regexes to decipher various types of errors - is the way to handle rejections in Spray so that it doesn't spit back ugly messages like this to API clients:
{
"message": "Missing required creator property 'value' (index 2)\n at [Source: {\n \"source\": \"k\",\n \"values\": [\n {\n \"dt\": \"2015-10-15T16:27:42.014Z\",\n \"id\":\"0022A3000004E6E1\",\n \n \"attribute\":\"a\",\n \"_id\": \"45809haoua\",\n \"__id\": \"a2p49t7ya4wop9h\",\n \"___id\": \"q2ph84yhtq4pthqg\"\n }]\n}; line: 12, column: 9] (through reference chain: io.keenhome.device.models.DatumList[\"values\"]->com.fasterxml.jackson.module.scala.deser.BuilderWrapper[0])"
}
How can I handle these error messages (eg return non-garbage to API clients) without having to do a regex on strings that are likely to change?
Ah I see. These crazy messages are generated by Jackson (not by Spray). So you have couple options:
1) Handle JSONObjectException in handleExceptions directive.
2) Or catch and transform JSONObjectException into your exception class before it reaches routing. Then I'd handle response with custom exception handler.
implicit def myExceptionHandler =
ExceptionHandler {
case ex: ExceptionWithUser => ctx => {
val user = ex.user
val cause = ex.getCause
logger.error(s"${cause.getClass.getSimpleName} for ${ex.user.name} [${ex.user.id}]", cause)
ctx.complete(StatusCodes.InternalServerError, ErrorResponse(ex.code, ex))
}
case ex: Throwable => ctx =>
logger.warning("Request {} could not be handled normally", ctx.request)
ctx.complete(StatusCodes.InternalServerError, ErrorResponse(StatusCodes.InternalServerError.intValue, ex))
}
and for rejections I use custom rejection handler that converts default text akka-http rejections to json response that API clients expect (I can show it if you like).
Related
This my script for minting NFT:
{-# INLINABLE mkPolicy #-}
mkPolicy :: BuiltinData -> PlutusV2.ScriptContext -> Bool
mkPolicy _ ctx = traceIfFalse "wrong amount minted" checkNFTAmount
where
info :: PlutusV2.TxInfo
info = PlutusV2.scriptContextTxInfo ctx
-- hasUTxO :: Bool
-- hasUTxO = any (\i -> PlutusV2.txInInfoOutRef i == mpTxOutRef r) $ PlutusV2.txInfoInputs info
checkNFTAmount :: Bool
checkNFTAmount = case Value.flattenValue (PlutusV2.txInfoMint info) of
[(cs, tn', amt)] -> cs == ownCurrencySymbol ctx && tn' == PlutusV2.TokenName "" && amt == 1
_ -> False
{-
As a Minting Policy
-}
compiledCode :: PlutusTx.CompiledCode (BuiltinData -> BuiltinData -> ())
compiledCode = $$(PlutusTx.compile [|| wrap ||])
where
wrap = Scripts.mkUntypedMintingPolicy mkPolicy
policy :: Scripts.MintingPolicy
policy = PlutusV2.mkMintingPolicyScript compiledCode
script :: PlutusV2.Script
script = PlutusV2.unMintingPolicyScript policy
{-
As a Short Byte String
-}
scriptSBS :: SBS.ShortByteString
scriptSBS = SBS.toShort . LBS.toStrict $ serialise script
{-
As a Serialised Script
-}
serialisedScript :: PlutusScript PlutusScriptV2
serialisedScript = PlutusScriptSerialised scriptSBS
writeSerialisedScript :: IO ()
writeSerialisedScript = void $ writeFileTextEnvelope "nft-mint-V2.plutus" Nothing serialisedScript
I'm using Mesh to minting NFT with that script
const walletAddr = wallet.getPaymentAddress();
const addressUtxo: UTxO[] = await provider.fetchAddressUTxOs(walletAddr);
const redeemer: Partial<Action> = {
tag: "MINT",
data: {
alternative: 0,
fields: [],
},
};
const assetMetadata: AssetMetadata = {
name: "MyNFT",
image: "https://picsum.photos/200",
mediaType: "image/jpg",
description: "This NFT is minted by me.",
};
const asset: Mint = {
assetName: "MyNFT",
assetQuantity: "1",
metadata: assetMetadata,
label: "721",
recipient: walletAddr,
};
// Mint NFT
const tx = new Transaction({ initiator: wallet });
tx.mintAsset(script, asset, redeemer);
tx.setCollateral([addressUtxo[0]]);
const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx, true);
try {
const txHash = await wallet.submitTx(signedTx);
console.log(txHash);
} catch (e) {
console.log(e);
}
Unfortunately, it returned with this error:
transaction submit error ShelleyTxValidationError ShelleyBasedEraBabbage
(ApplyTxError
[UtxowFailure
(UtxoFailure
(FromAlonzoUtxoFail
(UtxosFailure
(ValidationTagMismatch
(IsValid True)
(FailedUnexpectedly
(PlutusFailure \\\"\\\\nThe 2 arg plutus script (PlutusScript PlutusV2 ScriptHash \\\\\\\"77f807bc9403ef0177cc2a9956bfd5628ee649680041ccf48a198fc0\\\\\\\") fails.
\\\\nCekError An error has occurred:
User error:\\\\nThe machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
\\\\nThe protocol version is: ProtVer {pvMajor = 7, pvMinor = 0}\\\\nThe redeemer is: Constr 0 []\\\\
nThe second data argument, does not decode to a context
Does anyone faced this error before? As the error said "The second data argument, does not decode to a context" and I specify context in my script like this, I think the problem is with the context param. Or what is wrong with my script.
So I have an API response like this :
If it succeed
{"success":true,"message":"user found","data": {data} }
If failed
{"success":false,"message":"user not found" }
the response code for the two above is 200.
After reading Ktor's documentation, we can use Response validation (https://ktor.io/docs/response-validation.html#2xx).
When I get a response with status = false, the validation is working properly.
But when I get response with status = true, I get this error
io.ktor.serialization.JsonConvertException: Illegal input
But if I don't use validationResponse, and get response API status = true everything is fine.
My code now
HttpResponseValidator {
validateResponse { response ->
val error: ErrorResponse = response.body()
if (!error.success) {
if (error.message.equals("bad_password", ignoreCase = true)) {
throw ResponseException.BadPassword
} else {
if (error.message.isEmpty()) {
throw ResponseException.BadRequest
} else {
throw ResponseException.UnknownError(
error = error.message
)
}
}
}
}
}
#Serializable
data class ErrorResponse(
#SerialName("message")
val message: String,
#SerialName("success")
val success: Boolean
)
is there any way to solve this ?
I'm trying to create a websocket client using tokio_tungstenite and mio but I couldn't initialize a stream because of handshake issues. Here is the code I have:
let addr: Vec<_> = ws_url
.to_socket_addrs()
.map_err(|err| ClientError {
message: err.to_string(),
})
.unwrap()
.collect();
println!("{:?}", addr);
let connector = TlsConnector::new().unwrap();
let stream = TcpStream::connect(addr[0]).unwrap();
let mut stream = match connector.connect(ws_url.as_str(), stream) {
Ok(stream) => Ok(stream),
Err(err) => match err {
native_tls::HandshakeError::Failure(err) => Err(ClientError::new(format!(
"Handshake failed: {}",
err.to_string()
))),
native_tls::HandshakeError::WouldBlock(mh) => match mh.handshake() {
Ok(stream) => Ok(stream),
Err(err) => Err(ClientError::new(format!( // <-- the handshake process was interrupted
"Handshake failed: {}",
err.to_string()
))),
},
},
}?;
This code fails on mh.handshake() with an error: the handshake process was interrupted.
Does anyone know why that happens and how to fix it?
After long research decided to not use mio completely and create an event loop manually. This is doable but too time consuming for a simple task I do.
If you were to create a single threaded event loop, you can just use tungstenite and set_nonblocking method of the underling socket:
let url = Url::parse(ws_url.as_str()).unwrap();
match tungstenite::connect(url) {
Ok((mut sock, _)) => {
let s = sock.get_mut();
match s {
tungstenite::stream::MaybeTlsStream::Plain(s) => s.set_nonblocking(true),
tungstenite::stream::MaybeTlsStream::NativeTls(s) => {
s.get_mut().set_nonblocking(true)
}
x => panic!("Received unknown stream type: {:?}", x),
}
.map_err(|err| ClientError::new(format!("Failed to unblock the socket: {}", err)))
.unwrap();
Ok(Box::new(KrakenWsClient { sock }))
}
Err(err) => Err(ClientError {
message: format!(
"Failed to establish websocket connection: {}",
err.to_string()
),
}),
}
Then reading will look like this:
fn next_message(&mut self) -> Result<Option<Message>> {
match self.sock.read_message() {
Ok(msg) => self.parse_msg(msg),
Err(err) => match err {
Error::Io(err) => {
if err.kind() == ErrorKind::WouldBlock {
Ok(None)
} else {
Err(ClientError::new(format!(
"Error reading from websocket: {}",
err
)))
}
}
_ => Err(ClientError::new(format!(
"Error reading from websocket: {}",
err
))),
},
}
}
Remember to control timing of yours event loop to prevent it using 100% of CPU. Hope this helps!
I am considering, how may I rewrite this code in more reactive way (without if's, throw Exceptions in intermediate steps etc., and best practice for logging intermediate results)
return identityRepository.findByDeviceIdAndToken(
deviceId,
authToken
).doOnSuccess(identity -> {
if (identity == null) {
log.info(
"Pair Auth-Token: {} and Device-ID: {} not found",
authToken,
deviceId
);
}
})
.map(MyPrincipal::new)
.map(
principal -> {
if (!principal.isCredentialsNonExpired()) {
throw new CredentialsExpiredException();
}
return
new UsernamePasswordAuthenticationToken(
principal,
null,
Collections.emptyList()
)
;
}
)
.flatMap(this.authenticationManager::authenticate)
.map(SecurityContextImpl::new);
You can use Optional to eliminate if condition from your code. There are total 2 if condition in your code.
Lets first remove if from doOnSuccess method
.doOnSuccess(identity -> {
if (identity == null) {
log.info(
"Pair Auth-Token: {} and Device-ID: {} not found",
authToken,
deviceId
);
}
})
You can use ifPresentOrElse to remove if condition. It was introduced in java 9:
.doOnSuccess(identity -> Optional.ofNullable(identity)
.ifPresentOrElse(
val -> {},
() -> log.info(
"Pair Auth-Token: {} and Device-ID: {} not found",
authToken,
deviceId)
)
)
The second if condition is in map method
.map(principal -> {
if (!principal.isCredentialsNonExpired()) {
throw new CredentialsExpiredException();
}
return new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList());
})
In above code, You are throwing some exception based on a condition. You can use filter along with orElseThrow to throw exception if Optional has become empty due to filter:
.map(principal -> Optional.of(new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()))
.filter(token -> token.getPrincipal().isCredentialsNonExpired())
.orElseThrow(CredentialsExpiredException::new))
You can change map logic in this way:
return identityRepository.findByDeviceIdAndToken(
deviceId,
authToken
).doOnSuccess(identity -> {
if (identity == null) {
log.info(
"Pair Auth-Token: {} and Device-ID: {} not found",
authToken,
deviceId
);
}
})
.map(MyPrincipal::new)
.filter(principal -> principal.isCredentialsNonExpired())
.switchIfEmpty(Mono.error(new CredentialsExpiredException()))
.map(x -> new UsernamePasswordAuthenticationToken(
principal,
null,
Collections.emptyList()
)
)
.flatMap(this.authenticationManager::authenticate)
.map(SecurityContextImpl::new);
I did write an Observable which is polling an URL which completes once a specific value is returned.
private checkPairingStatus(paringModel: any): Observable<ResponseObject> {
let data = { id: 1234 };
return Observable
.interval(2000)
.switchMap(() => this.get<ResponseObject>('http://api/getstatus', data))
.first(r => r.Status === 'success') // once our pairing is active we emit that
.timeout(90000, Observable.throw(new Error('Timeout ocurred')));
// todo: find a way to abort the interval once the PairingStatus hits the 'canceled' status.
}
This works pretty fine but I'm struggling on how to throw an exception once my respone for example hits the following status "r.Status === 'canceled'".
Thanks for any hint on that!
Regards
Lukas
You can just use do() and throw an Error with whatever condition you need:
return Observable
.interval(200)
.do(val => {
if (val == 5) {
throw new Error('everything is broken');
}
})
.subscribe(
val => console.log(val),
err => console.log('Error:', err.message)
);
This prints to console:
0
1
2
3
4
Error: everything is broken
In your case you'll want to test a condition such as r.Status === 'canceled' or whatever.