qdsfdhvh / compose-imageloader

Compose Image library for Kotlin Multiplatform.
https://qdsfdhvh.github.io/compose-imageloader/
MIT License
404 stars 15 forks source link

Loading images in browser MissingAllowOriginHeader issue #446

Open florind opened 5 months ago

florind commented 5 months ago

Hi,

Hi, I am building a compose app that should load images from a 3rd party website in a grid. I am using the 1.7.2-SNAPSHOT from https://github.com/qdsfdhvh/compose-imageloader/issues/151. It works as expected for all native platforms but in the browser, I get this error: Cors-Origin Resource Sharing error: MissingAllowOriginHeader . When sending the image request, the browser adds an Origin and a Referer header. This seems relevant because the browser app I'm rewriting, doesn't send these two headers and images load correctly. I think setting mode no-cors in the fetch API should solve the issue but I don't know how to do this using imageloader's API. Any help is appreciated. Thanks! florin

qdsfdhvh commented 5 months ago

Currently, You can customise the ktorClient:

val imageLoader = ImageLoader {
    // ...
    components {
        setupDefaultComponents(
            httpClient = {
                HttpClient(/* OkHttp.create() or other */) {
                    // some ktor config
                }
            }
        )
    }
}

If you need to pass some parameters from the ImageRequest, you need to customise the ktor fetcher with the help of the Options.extra:

val imageRequest = ImageRequest {
    // ...
    options {
        extra {
            put("headerMap", mapOf("key1" to "value1"))
        }
    }
}
// ...
import io.ktor.http.Url

class CustomKtorUrlFetcher(
    private val httpUrl: Url,
    private val headerMap: Map<String, String>,
    httpClientFactory: () -> HttpClient,
) : Fetcher {

    private val httpClient by lazy(httpClientFactory)

    override suspend fun fetch(): FetchResult {
        val response = httpClient.request {
            url(httpUrl)
            headers {
                headerMap.forEach { (key, value) ->
                    append(key, value)
                }
            }
        }
        if (!response.status.isSuccess()) {
            error("code:${response.status.value}, ${response.status.description}")
        }
        return FetchResult.OfSource(
            source = Buffer().apply {
                write(response.bodyAsChannel().toByteArray())
            },
            extra = extraData {
                mimeType(response.contentType()?.toString())
            },
        )
    }
    class Factory(private val httpClientFactory: () -> HttpClient) : Fetcher.Factory {
        override fun create(data: Any, options: Options): Fetcher? {
            if (data !is Url) return null
            val headerMap = options.extra["headerMap"] as Map<String, String>
            return CustomKtorUrlFetcher(
                httpUrl = data,
                headerMap = headerMap,
                httpClientFactory = httpClientFactory,
            )
        }
    }
}

// Use

val imageLoader = ImageLoader {
    // ...
    components {
        add(CustomKtorUrlFetcher.Factory(
            httpClientFactory = {
                HttpClient(/* OkHttp.create() or other */) {
                    // some ktor config
                }
            }
        ))
        setupDefaultComponents()
    }
}
joreilly commented 3 months ago

@florind were you able to make any more progress with this?

florind commented 3 months ago

I ended up creating an image proxy:

wf_client = httpx.AsyncClient(base_url="base-url-for-images")
@app.get("{path:path}.png")
async def serve_image(path: str):
    url = httpx.URL(path=f"{path}.png")
    print(url)
    rp_req = wf_client.build_request("GET", url)
    rp_resp = await wf_client.send(rp_req, stream=True)

    return StreamingResponse(
        rp_resp.aiter_raw(),
        status_code=rp_resp.status_code,
        headers=rp_resp.headers,
        background=BackgroundTask(rp_resp.aclose),
    )