Ktor REST API

REST - Respresentational State Transfer

Ktor and Hypermedia

Hypermedia is an extension of the term hypertext, it is a medium of delivering information to a browser that could include graphics, audio, video, plaintext, and hyperlinks.

Content Negotiation

install(ContentNegotiation) {
	register(TheContentType,  TheContentTypeConverter()) {
		//configure converter
	}
}

Custom Converters

interface ContentConverter {
	suspend fun convertForSend(context: PipelineContent<Any, ApplicationCall>, contentType: ContentType, value: Any): Any?
	suspend fun convertForReceive(context: PipelineContext<ApplicationReceiveRequest, ApplicationCall>): Any?
}

An example with the jackson XML library:

class XmlConverter : ContentConverter {
    override suspend fun convertForSend(
        context: PipelineContext<Any, ApplicationCall>,
        contentType: ContentType,
        value: Any
    ): Any? {
        val xmlMapper = XmlMapper()
        val xml = xmlMapper.writeValueAsString(value)
        return TextContent(xml, contentType.withCharset(context.call.suitableCharset()))
    }

    override suspend fun convertForReceive(context: PipelineContext<ApplicationReceiveRequest, ApplicationCall>): Any? {
        val request = context.subject
        val channel = request.value as? ByteReadChannel ?: return null
        val reader = channel.toInputStream().reader(context.call.request.contentCharset() ?: Charsets.UTF_8)
        val type = request.typeInfo
        val xmlMapper = XmlMapper()
        val xml = reader
        val result: Any? = xmlMapper.readValue(xml, type.javaClass)
        return result
    }
}

And then set that in the ContentNegotiator

    // If nothing is specified in the header the request will use XML
    install(ContentNegotiation) {
        gson {
        }
        register(ContentType.Application.Xml, XmlConverter())
    }

By setting up both json and xml we can set the perfered response type in the header: curl -H "Accept: application/json" "http://localhost:8080/spaceship" curl -H "Accept: application/xml" "http://localhost:8080/spaceship"

Serialization

    install(ContentNegotiation) {
        // If this line is active the request will use XML, unless the header specifies otherwise
        //register(ContentType.Application.Xml, XmlConverter())
        jackson {
            registerModule(JavaTimeModule())
            enable(SerializationFeature.INDENT_OUTPUT)
            enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)
        }
    }

How Data Is Posted to the Route Endpoint

val channel: ByteReadChannel = call.receiveChannel()
val text: String = call.receiveText()
// receiveStream() is synchronous and blocks the thread
val inputStream: InputStream = call.receiveStream()
val multipart: MultiPartData = call.receiveMultipart()
val multipart = call.receiveMultipart()
multipart.forEachPart { ..}

Routes with Path Variables

get<MyLocation> {
	call.respondText("${it.mypathvar}")
}

Nested Routes

@Location("/book/{category}")
data class Book(val category: String) {
	@Location("/{author}")
	data class Author(val book: Book, val author: String) 
	
	@Location("/list")
	data class List(val book: Book)
}

Routes with Request Parameters

Working with Headers

Retrieve Headers from Request

Setting Headers on a Response

install(DefaultHeaders) {
	header("SystemName", "BookStore")
}

Error Handling and Authentication

install(StatusPages) {
	exception<MyCustomException> { cause ->
		call.respond(HttpStatusCode.InternalServerError, "Whoops a Cusom Error Occurred")
		throw cause
	}
}
install(StatusPages) {
	exception<HttpRedirectException> { e ->
		call.respondRedirect(e.location, permanent = e.permanent)
	}
}

class HttpRedirectException(val location: String, val permanent: Boolean = false): RuntimeException()
install(StatusPages) {
	status(HttpStatusCode.NotFound) {
		call.respond(TextContent("${it.value} ${it.description}",
		ContentType.Text.Plain.withCharsets(Charsets.UTF_8), it))
	}
}

install(StatusPages) {
	statusFile(HttpStatusCode.NotFound, HttpStatusCode.Unauthorized, FilePattern = "my-custom-error#.html")
}
// The # above will be filled with the error code number

Authentication Concepts

Supported Authentication Methods

    install(Authentication) {
        basic("myAuth1") {
            realm = " My Realm"
            validate {
                if (it.name == "mike" && it.password == "password")
                    UserIdPrincipal(it.name)
                else null
            }
        }
        basic("myAuth2") {
            realm = "MyOtherRealm"
            validate {
                if(it.password == "${it.name}abc123")
                    UserIdPrincipal(it.name)
                else
                    null
            }
        }
    }
	
	routing {

        authenticate("myAuth1") {
            get("/secret/weather") {
                val principal = call.principal<UserIdPrincipal>()!!
                call.respondText("Hello ${principal.name} it is secretly going to rain today")
            }
        }
        
        authenticate("myAuth2") {
            get("/secret/color") {
                val principal = call.principal<UserIdPrincipal>()!!
                call.respondText("Hello ${principal.name}, green is going to be popular tomorrow")
            }
        }
}

Routing Interceptors - Check Admin Rights

	// creating a new interceptor to be called after the call
    val mike = PipelinePhase("Mike")
	// This would not work if it was insertPhaseAfter, as the route would have already provided a response
    insertPhaseBefore(ApplicationCallPipeline.Call, mike)
	
    intercept(ApplicationCallPipeline.Setup) {
        log.info("Setup phase")
    }
    intercept(ApplicationCallPipeline.Call) {
        log.info("Call phase")
    }
    intercept(ApplicationCallPipeline.Features) {
        log.info("Features phase")
    }
    intercept(ApplicationCallPipeline.Monitoring) {
        log.info("Monitoring phase")
    }

    intercept(mike) {
        log.info("Mike Phase${call.request.uri}")
        if (call.request.uri.contains("mike")) {
            log.info("The uri contains mike")
			call.respondText("The Endpoint contains mike")
			// finish means the remaining interceptors will not be called
            finish()
        }
    }

    routing {

        get("/something/mike/something") {
            call.respondText("Endpoint handled by route.")
        }
    }   

Revision #1
Created 17 April 2022 00:22:01 by Elkip
Updated 17 April 2022 01:02:09 by Elkip