Ktor doesn't log exceptions - kotlin

Ktor (1.4.2) seems to be suppresing exceptions. According to documentation I need to re-throw exception like this
install(StatusPages) {
exception<Throwable> { ex ->
val callstack: String = ....
call.respond(HttpStatusCode.InternalServerError,
ApiError(ex.javaClass.simpleName, ex.message ?: "Empty Message", callstack)
)
throw ex
}
}
I wrote wrapper around Logger interface to make sure it's calling Logger.error() (it doesn't) and configured it like this
install(CallLogging) {
level = org.slf4j.event.Level.INFO
logger = Log4j2Wrapper(logger2)
}
I can see INFO-level logs. What am I missing ?

Turned out the problem is in rather akward design of ktor api. Summary of my findings:
Contrary to common expectation to see log events of specified log level and above it doesn't that way with CallLogging. It expects exact match of the logging level which minimizes usefulness of this feature.
You need to overwrite logger specified in ApplicationEnvironment.log. But if you follow official ktor examples and do it like this
val server = embeddedServer(Netty, port) {
applicationEngineEnvironment {
log = YOUR_LOGGER
}
}
it won't work because value call.application.environment inside function io.ktor.server.engine.defaultEnginePipeline(env) is still original one, created from logger name in config file or NOP logger.
Hence you'll need to initialize your server like this
val env = applicationEngineEnvironment {
log = YOUR_LOGGER
module {
install(CallLogging) {
level = org.slf4j.event.Level.INFO
}
install(StatusPages) {
exception<Throwable> { ex ->
val callstack: String = ...
call.respond(HttpStatusCode.InternalServerError,
ApiError(ex.javaClass.simpleName, ex.message ?: "Empty Message", callstack)
)
throw ex
}
}
routing {
}
}
connector {
host = "0.0.0.0"
port = ...
}
}
val server = embeddedServer(Netty, env)

Related

How to get original service's exception on Apache Ignite client?

Ley's say we have a service:
interface ExceptionService : Service {
fun doSmth(flag: Boolean)
}
implementation:
class ExceptionServiceImpl : ExceptionService {
override fun doSmth(flag: Boolean) {
if (flag) {
throw IOException("my IO exception")
} else {
throw IllegalArgumentException("my IllegalArgument exception")
}
}
}
We deploy it onto Ignite cluster:
Ignition.start(
IgniteConfiguration()
.setServiceConfiguration(
ServiceConfiguration()
.setService(ExceptionServiceImpl())
.setName("service")
)
)
And now we call it from client:
val client = Ignition.startClient(ClientConfiguration().setAddresses("127.0.0.1"))
val service = client.services().serviceProxy("service", ExceptionService::class.java)
try {
service.doSmth(true)
} catch (e: Exception) {
println(e.mesaage)
}
try {
service.doSmth(false)
} catch (e: Exception) {
println(e.mesaage)
}
Console output will be:
Ignite failed to process request [1] my IO exception (server status code [1])
Ignite failed to process request [2] my IllegalArgument exception (server status code [1])
The problem is that the type of the caught exception is always org.apache.ignite.client.ClientException. The only thing that left from the original exceptions thrown on server is message, and even it is wrapped in other words. The cause is of type org.apache.ignite.internal.client.thin.ClientServerError. Types are lost.
I want to handle on client side different types of exceptions thrown by service. Is there a way to do it? Maybe some Ignite configuration that I missed?
Try enabling ThinClientConfiguration#sendServerExceptionStackTraceToClient

Exception shows up in console although try...catch must prevent it

In my minimal API I try to save entity to database. The table contains UNIQUE constraint on license_plate column, so DbUpdateException would be thrown if same license plate would be passed in. I used try..catch in order to handle this situation:
app.MapPost("/vehicles", (VehiclesContext db, Vehicle vehicle) =>
{
var entity = db.Add(vehicle);
try
{
db.SaveChanges();
return Results.CreatedAtRoute("GetVehicle", new { inventoryNumber = entity.Entity.InventoryNumber }, entity.Entity);
}
catch (DbUpdateException)
{
var error = new JsonObject
{
["error"] = $"Creating vehicle failed because such license plate already exists: {vehicle.LicensePlate}"
};
return Results.BadRequest(error);
}
}).AddFilter<ValidationFilter<Vehicle>>();
However, when I pass duplicate license plate, I see this exception in console:
So, why does this exception show up in console? I tried to play with LogLevel for Microsoft.AspNetCore in appsettings.json (and appsettings.Development.json also) by changing Warning to Information, but still exceptions shows up.
The exception is logged prior to throwing, so you cannot stop the logging mechanism from being invoked.
However, you should be able to control output using LogLevel.
Note that the log comes from "Microsoft.EntityFrameworkCore" not "Microsoft.AspNetCore".
I just don't want to see errors which I handle in try...catch block!
Do you mean you don't want to see the fail ? Use Try-catch in minimal API?
Below is a demo, you can refer to it.
without try-catch
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>{
string s = null;
if (s == null)
{
throw new ArgumentNullException(paramName: nameof(s), message: "parameter can't be null.");
}}
);
app.Run();
result:
use try-catch:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>{
try
{
string s = null;
if (s == null)
{
throw new ArgumentNullException(paramName: nameof(s), message: "parameter can't be null.");
}
}
catch (Exception e)
{
Console.WriteLine("{0} Exception caught.", e);
}
}
);
app.Run();
result:

How do I get access to errors in custom Ktor JWT challenge?

I'd like to be able to respond differently to different JWTAuth errors, so have customised the challenge (below is some code I've been using to test). Unfortunately the context is returning an empty list for the errors. I've tested with an expired token and with no token at all.
fun JWTAuthenticationProvider.Configuration.customConfigure() {
verifier(verifier)
realm = ISSUER
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, JSONObject(mapOf("err" to context.authentication.allErrors)))
}
validate {
if (it.payload.audience.contains(AUDIENCE)) {
it.payload.getClaim("id").asString().let { id -> userDao.getUserById(id) }
} else null
}
}
I was wondering if I was perhaps missing something. Hoping someone can help!
Try to use the validate block, you can check the error by using the payload and respond by throwing an exception using Status Pages feature
Maybe not the most elegant solution, but it does what I need it to do, so it's fine for now. I used the custom challenge to throw an exception with a specific error message depending on the error.
fun JWTAuthenticationProvider.Configuration.customConfigure() {
verifier(verifier)
realm = ISSUER
challenge { _, _ ->
// get custom error message if error exists
val errorMessage = call.request.headers["Authorization"]?.let {
if (it.isNotEmpty()) {
try {
val jwt = it.replace("Bearer ", "")
verifier.verify(jwt)
""
} catch (e: Exception) {
when (e) {
is JWTVerificationException ->
if (e.localizedMessage.contains("expired")) "Token expired" else "Invalid token"
else -> "Unknown token error"
}
}
} else "Authorization token empty"
} ?: "No authorization header"
// if error throw UnauthorizedException
if (errorMessage.isNotEmpty()) {
throw UnauthorizedException(errorMessage)
}
}
validate {
if (it.payload.audience.contains(AUDIENCE)) {
it.payload.getClaim("id").asString().let { id -> userDao.getUserById(id) }
} else null
}
}
Then as suggested I have used the status pages feature to respond.
// catch authorization exception
exception<UnauthorizedException> {
call.respond(HttpStatusCode.Unauthorized, JSONObject(mapOf("err" to it.message)))
}

Use Spring Cloud Spring Service Connector with RabbitMQ and start publisher config function

I connect RabbitMQ with sprin cloud config:
#Bean
public ConnectionFactory rabbitConnectionFactory() {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("publisherConfirms", true);
RabbitConnectionFactoryConfig rabbitConfig = new RabbitConnectionFactoryConfig(properties);
return connectionFactory().rabbitConnectionFactory(rabbitConfig);
}
2.Set rabbitTemplate.setMandatory(true) and setConfirmCallback():
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMandatory(true);
template.setMessageConverter(new Jackson2JsonMessageConverter());
template.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
System.out.println("send message failed: " + cause + correlationData.toString());
} else {
System.out.println("Publisher Confirm" + correlationData.toString());
}
});
return template;
}
3.Send message to queue to invoke the publisherConfirm and print log.
#Component
public class TestSender {
#Autowired
private RabbitTemplate rabbitTemplate;
#Scheduled(cron = "0/5 * * * * ? ")
public void send() {
this.rabbitTemplate.convertAndSend(EXCHANGE, "routingkey", "hello world",
(Message m) -> {
m.getMessageProperties().setHeader("tenant", "aaaaa");
return m;
}, new CorrelationData(UUID.randomUUID().toString()));
Date date = new Date();
System.out.println("Sender Msg Successfully - " + date);
}
}
But publisherConfirm have not worked.The log have not been printed. Howerver true or false, log shouldn't been absent.
Mandatory is not needed for confirms, only returns.
Some things to try:
Turn on DEBUG logging to see it it helps; there are some logs generated regarding confirms.
Add some code
.
template.execute(channel -> {
system.out.println(channel.getClass());
return null;
}
If you don't see PublisherCallbackChannelImpl then it means the configuration didn't work for some reason. Again DEBUG logging should help with the configuration debugging.
If you still can't figure it out, strip your application to the bare minimum that exhibits the behavior and post the complete application.

How do I force a method in Groovy to throw an exception

I wanted to write a test for a method in Groovy that throws an IOException. The only way for me to simulate this in the test is to force the method to throw this exception
This is what the original code looks like:
public void cleanUpBermudaFiles(RequestMessage requestMessage)
{
final File sourceDirectory = new File(preferenceService.getPreference("bermuda.landingstrip") + File.separator + requestMessage.getWorkflowId().getValue());
if(sourceDirectory!=null && sourceDirectory.exists())
{
deleteDirectory(sourceDirectory);
}
else
{
LOG.error("Directory must exist in order to delete");
}
}
private void deleteDirectory(File directoryToDelete)
{
try {
FileUtils.deleteDirectory(directoryToDelete);
} catch (Exception e) {
LOG.error("Failed to delete Bermuda files directory located at:" + directoryToDelete.getPath() + "with an exception" + e.getMessage());
}
}
MY TEST: (I'm looking for a way to make deleteDirectory throw IOException)
public void testCleanUpBermudaFailure()
{
workflowId = new WorkflowId("123456")
workflowDirectory = new File(srcDirectory, workflowId.value)
workflowDirectory.mkdir()
File.createTempFile('foo','.lst', workflowDirectory)
def exception = {throw new IOException()}
expect(mockRequestMessage.getWorkflowId()).andReturn(workflowId)
expect(mockPreferenceService.getPreference("bermuda.landingstrip")).andReturn(srcDirectory.path)
replay(mockPreferenceService, mockRequestMessage)
fileCleanUpService.preferenceService = mockPreferenceService
fileCleanUpService.metaClass.deleteDirectory = exception
fileCleanUpService.cleanUpBermudaFiles(mockRequestMessage)
verify(mockPreferenceService, mockRequestMessage)
assert srcDirectory.listFiles().length == 0, 'CleanUp failed'
}
If the service class is a Groovy class, you would want to mock FileUtils like:
FileUtils.metaClass.static.deleteDirectory = { File f -> throw new IOException() }
However, as ataylor pointed out, you cannot intercept calls if it's a Java class. You can find a nice blog post about it here.
You are mocking a no-arg call to deleteDirectory, but the real deleteDirectory takes one argument of type File. Try this:
def exception = { File directoryToDelete -> throw new IOException() }
...
fileCleanUpService.metaClass.deleteDirectory = exception