Skip to content

Commit

Permalink
KTOR-6655 Fix missing charset for static js, css and svg resources (#…
Browse files Browse the repository at this point in the history
…4058)

(cherry picked from commit c892bc0)
  • Loading branch information
marychatte committed Jun 19, 2024
1 parent b204ed7 commit fcbb340
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 57 deletions.
25 changes: 24 additions & 1 deletion ktor-http/common/src/io/ktor/http/FileContentType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,34 @@ private val extensionsByContentType: Map<ContentType, List<String>> by lazy {
internal fun List<ContentType>.selectDefault(): ContentType {
val contentType = firstOrNull() ?: ContentType.Application.OctetStream
return when {
contentType.contentType == "text" && contentType.charset() == null -> contentType.withCharset(Charsets.UTF_8)
contentType.match(ContentType.Text.Any) -> contentType.withCharsetUTF8IfNeeded()
contentType.match(ContentType.Image.SVG) -> contentType.withCharsetUTF8IfNeeded()
contentType.matchApplicationTypeWithCharset() -> contentType.withCharsetUTF8IfNeeded()
else -> contentType
}
}

private fun ContentType.matchApplicationTypeWithCharset(): Boolean {
if (!match(ContentType.Application.Any)) return false

return when {
match(ContentType.Application.Atom) ||
match(ContentType.Application.JavaScript) ||
match(ContentType.Application.Rss) ||
match(ContentType.Application.Xml) ||
match(ContentType.Application.Xml_Dtd)
-> true

else -> false
}
}

private fun ContentType.withCharsetUTF8IfNeeded(): ContentType {
if (charset() != null) return this

return withCharset(Charsets.UTF_8)
}

internal fun <A, B> Sequence<Pair<A, B>>.groupByPairs() = groupBy { it.first }
.mapValues { e -> e.value.map { it.second } }

Expand Down
1 change: 1 addition & 0 deletions ktor-http/common/src/io/ktor/http/Mimes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ private val rawMimes: String
.jpgv,video/jpeg
.jpm,video/jpm
.jps,image/x-jps
.js,text/javascript
.js,application/javascript
.json,application/json
.jut,image/jutvision
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,135 +12,132 @@ import io.ktor.server.plugins.conditionalheaders.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import io.ktor.util.*
import kotlin.test.*

@Suppress("DEPRECATION")
class WebjarsTest {

@Test
fun resourceNotFound() {
withTestApplication {
application.install(Webjars)
handleRequest(HttpMethod.Get, "/webjars/foo.js").let { call ->
testApplication {
install(Webjars)
client.get("/webjars/foo.js").let { response ->
// Should be handled by some other routing
assertEquals(HttpStatusCode.NotFound, call.response.status())
assertEquals(HttpStatusCode.NotFound, response.status)
}
}
}

@Test
fun pathLike() {
withTestApplication {
application.install(Webjars)
application.routing {
testApplication {
install(Webjars)
routing {
get("/webjars-something/jquery") {
call.respondText { "Something Else" }
}
}
handleRequest(HttpMethod.Get, "/webjars-something/jquery").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("Something Else", call.response.content)
client.get("/webjars-something/jquery").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("Something Else", response.bodyAsText())
}
}
}

@Test
fun nestedPath() {
withTestApplication {
application.install(Webjars) {
testApplication {
install(Webjars) {
path = "/assets/webjars"
}
handleRequest(HttpMethod.Get, "/assets/webjars/jquery/jquery.js").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("application/javascript", call.response.headers["Content-Type"])
client.get("/assets/webjars/jquery/jquery.js").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals(ContentType.Text.JavaScript, response.contentType()?.withoutParameters())
}
}
}

@Test
fun rootPath() {
withTestApplication {
application.install(Webjars) {
testApplication {
install(Webjars) {
path = "/"
}
handleRequest(HttpMethod.Get, "/jquery/jquery.js").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("application/javascript", call.response.headers["Content-Type"])
client.get("/jquery/jquery.js").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals(ContentType.Text.JavaScript, response.contentType()?.withoutParameters())
}
}
}

@Test
fun rootPath2() {
withTestApplication {
application.install(Webjars) {
testApplication {
install(Webjars) {
path = "/"
}
application.routing {
routing {
get("/") { call.respondText("Hello, World") }
}
handleRequest(HttpMethod.Get, "/").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("Hello, World", call.response.content)
client.get("/").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("Hello, World", response.bodyAsText())
}
handleRequest(HttpMethod.Get, "/jquery/jquery.js").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("application/javascript", call.response.headers["Content-Type"])
client.get("/jquery/jquery.js").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals(ContentType.Text.JavaScript, response.contentType()?.withoutParameters())
}
}
}

@Test
fun versionAgnostic() {
withTestApplication {
application.install(Webjars)
testApplication {
install(Webjars)

handleRequest(HttpMethod.Get, "/webjars/jquery/jquery.js").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("application/javascript", call.response.headers["Content-Type"])
client.get("/webjars/jquery/jquery.js").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals(ContentType.Text.JavaScript, response.contentType()?.withoutParameters())
}
}
}

@Test
fun withGetParameters() {
withTestApplication {
application.install(Webjars)
testApplication {
install(Webjars)

handleRequest(HttpMethod.Get, "/webjars/jquery/jquery.js?param1=value1").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("application/javascript", call.response.headers["Content-Type"])
client.get("/webjars/jquery/jquery.js?param1=value1").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals(ContentType.Text.JavaScript, response.contentType()?.withoutParameters())
}
}
}

@Test
fun withSpecificVersion() {
withTestApplication {
application.install(Webjars)
testApplication {
install(Webjars)

handleRequest(HttpMethod.Get, "/webjars/jquery/3.6.4/jquery.js").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("application/javascript", call.response.headers["Content-Type"])
client.get("/webjars/jquery/3.6.4/jquery.js").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals(ContentType.Text.JavaScript, response.contentType()?.withoutParameters())
}
}
}

@Test
fun withConditionalHeaders() {
withTestApplication {
application.install(Webjars)
application.install(ConditionalHeaders)
handleRequest(HttpMethod.Get, "/webjars/jquery/3.6.4/jquery.js").let { call ->
assertEquals(HttpStatusCode.OK, call.response.status())
assertEquals("application/javascript", call.response.headers["Content-Type"])
assertNotNull(call.response.headers["Last-Modified"])
fun withConditionalAndCachingHeaders() {
testApplication {
install(Webjars)
install(ConditionalHeaders)
client.get("/webjars/jquery/3.6.4/jquery.js").let { response ->
assertEquals(HttpStatusCode.OK, response.status)
assertEquals(ContentType.Text.JavaScript, response.contentType()?.withoutParameters())
assertNotNull(response.headers["Last-Modified"])
}
}
}

@OptIn(InternalAPI::class)
@Test
fun callHandledBeforeWebjars() {
val alwaysRespondHello = object : Hook<Unit> {
Expand All @@ -161,7 +158,7 @@ class WebjarsTest {
val response = client.get("/webjars/jquery/3.3.1/jquery.js")
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("Hello", response.bodyAsText())
assertNotEquals("application/javascript", response.headers["Content-Type"])
assertNotEquals(ContentType.Text.JavaScript, response.contentType()?.withoutParameters())
}
}
}
Empty file.
Empty file.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,27 @@ class StaticContentTest {
assertEquals("br", result.headers[HttpHeaders.ContentEncoding].orEmpty())
}
}

@Test
fun testCharset() = testApplication {
val fileName = "file"
val extensions = mapOf(
"js" to ContentType.Text.JavaScript,
"css" to ContentType.Text.CSS,
"svg" to ContentType.Image.SVG,
"xml" to ContentType.Application.Xml,
)

routing {
staticResources("/", "public/types")
}

extensions.forEach { (extension, contentType) ->
client.get("/$fileName.$extension").apply {
assertEquals(contentType.withCharset(Charsets.UTF_8), contentType())
}
}
}
}

private fun String.replaceSeparators() = replace("/", File.separator)
Expand Down

0 comments on commit fcbb340

Please sign in to comment.