I have burned a couple of weeks trying to get Reac...
# ktor
t
I have burned a couple of weeks trying to get React talking to Ktor. I can do a dead simple GET, but as soon as I start trying to implement POST end points, I get preflight OPTIONS returning Forbidden. I have tried to specify, both explicitly and by wild card every header I find in the browser developer tools, but to no avail. I have done everything I can find here, in searches, in multiple ChatBots, and still always the same result
Sorry, I meant to post everything in the thread, and dumped it in the main channel instead. Here it all is:
Copy code
package org.tatrc

import io.ktor.http.*
import io.ktor.serialization.gson.*
import io.ktor.server.application.*

import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.netty.handler.codec.DefaultHeaders
import org.tatrc.plugins.*

var receivedName = Name( "unknown", "unknown" )

fun main() {
    //embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module).start(wait = true)
    val environment = applicationEngineEnvironment {
        connector {
            port = 8080
        }
        module(Application::module)
    }
    embeddedServer(Netty, environment).start(wait = true)
}

fun Application.module() {
    //configureHTTP()
    //configureSecurity()
    //configureRouting()



    routing {
        install(ContentNegotiation) {
            gson()
        }
        install(CORS) {
            allowHeader(HttpHeaders.ContentType)
            allowHeader(HttpHeaders.Authorization)
            allowHeader(HttpHeaders.AccessControlAllowOrigin)
            allowHeader(HttpHeaders.Origin)
/*
            allowHeader(HttpHeaders.Accept)
            allowHeader(HttpHeaders.AcceptCharset)
            allowHeader(HttpHeaders.AcceptEncoding)
            allowHeader(HttpHeaders.AcceptLanguage)
            allowHeader(HttpHeaders.AcceptRanges)

            allowHeader(HttpHeaders.Referrer)

            allowHeader("Access-Control-Request-Headers")
            allowHeader("Access-Control-Request-Method")
            allowHeader("Connection")
            allowHeader("DNT")
            allowHeader("Host")
            allowHeader("Referer")
            allowHeader("Sec-Fetch-Dest")
            allowHeader("Sec-Fetch-Mode")
            allowHeader("Sec-Fetch-Site")
            allowHeader("Sec-GPC")
            allowHeader("User-Agent")

allowHeader("Accept")
allowHeader("AcceptCharset")
allowHeader("Accept-Encoding")
allowHeader("Accept-Language")
allowHeader("AcceptRanges")
allowHeader("Age")
allowHeader("Allow")
allowHeader("ALPN")
allowHeader("AuthenticationInfo")
allowHeader("Authorization")
allowHeader("CacheControl")
allowHeader("Connection")
allowHeader("ContentDisposition")
allowHeader("ContentEncoding")
allowHeader("ContentLanguage")
allowHeader("ContentLength")
allowHeader("ContentLocation,")
allowHeader("ContentRange")
allowHeader("ContentType")
allowHeader("Cookie")
allowHeader("DASL")
allowHeader("Date")
allowHeader("DAV")
allowHeader("Depth")
allowHeader("Destination")
allowHeader("ETag")
allowHeader("Expect")
allowHeader("Expires")
allowHeader("From")
allowHeader("Forwarded")
allowHeader("Host")
allowHeader("HTTP2Settings")
allowHeader("If")
allowHeader("IfMatch")
allowHeader("IfModifiedSince")
allowHeader("IfNoneMatch")
allowHeader("IfRange")
allowHeader("IfScheduleTagMatch")
allowHeader("IfUnmodifiedSince")
allowHeader("LastModified")
allowHeader("Location")
allowHeader("LockToken")
allowHeader("Link")
allowHeader("MaxForwards")
allowHeader("MIMEVersion")
allowHeader("OrderingType")
allowHeader("Origin")
allowHeader("Overwrite")
allowHeader("Position")
allowHeader("Pragma")
allowHeader("Prefer")
allowHeader("PreferenceApplied")
allowHeader("ProxyAuthenticate")
allowHeader("ProxyAuthenticationInfo")
allowHeader("ProxyAuthorization")
allowHeader("PublicKeyPins")
allowHeader("PublicKeyPinsReportOnly")
allowHeader("Range")
allowHeader("Referrer")
allowHeader("Referer")
allowHeader("RetryAfter")
allowHeader("ScheduleReply")
allowHeader("ScheduleTag")
allowHeader("SecWebSocketAccept,")
allowHeader("SecWebSocketExtensions")
allowHeader("SecWebSocketKey")
allowHeader("SecWebSocketProtocol")
allowHeader("SecWebSocketVersion")
allowHeader("Server")
allowHeader("SetCookie")
allowHeader("SLUG")
allowHeader("StrictTransportSecurity")
allowHeader("TE")
allowHeader("Timeout")
allowHeader("Trailer")
allowHeader("TransferEncoding")
allowHeader("Upgrade")
allowHeader("UserAgent")
allowHeader("Vary")
allowHeader("Via")
allowHeader("Warning")
allowHeader("WWWAuthenticate")
allowHeader("AccessControlAllowOrigin")
allowHeader("AccessControlAllowMethods")
allowHeader("AccessControlAllowCredentials")
allowHeader("AccessControlAllowHeaders")
allowHeader("AccessControlRequestMethod")
allowHeader("AccessControlRequestHeaders")
allowHeader("AccessControlExposeHeaders")
allowHeader("AccessControlMaxAge")
allowHeader("XHttpMethodOverride")
allowHeader("XForwardedHost")
allowHeader("XForwardedServer")
allowHeader("XForwardedProto")
allowHeader("XForwardedFor")
allowHeader("XRequestId")
allowHeader("XCorrelationId")
            */
            allowHeader("*")
            allowMethod(HttpMethod.Get)
            allowMethod(<http://HttpMethod.Post|HttpMethod.Post>)
            allowMethod(HttpMethod.Put)
            allowMethod(HttpMethod.Delete)
            allowMethod(HttpMethod.Options)
            allowCredentials = true
            allowHost("localhost:3000", schemes = listOf("http"))
            allowHost("127.0.0.1:3000", schemes = listOf("http"))
            //anyHost()
        }
        //install(DefaultHeaders)

        val allowedOrigins = listOf("<https://your-frontend.com>")
        val allowedMethods = listOf(HttpMethod.Get, <http://HttpMethod.Post|HttpMethod.Post>)
        val allowedHeaders = listOf(HttpHeaders.ContentType, HttpHeaders.Authorization)

        get ("/") {
            call.respondText("Hello from Hello", ContentType.Text.Plain)
        }

        get("/submit_name") {
            val name = "Your name is ${receivedName.name} ${receivedName.surname}\n"
            call.respondText(name, ContentType.Text.Plain, status = HttpStatusCode.OK)
        }

        post("/submit_name") {
            println("---- POST submit_name")
            call.request.headers.forEach { s, strings ->
                println("---------- header $s: $strings")
            }
            val contentType = call.request.contentType()
            println("-------- contentType: $contentType")
            receivedName = when {
                //contentType.match(ContentType.Application.Json) -> call.receive<Name>()
                contentType.match(ContentType.Application.FormUrlEncoded) -> {
                    call.request.queryParameters.forEach { s, strings ->
                        println("---------- queryParam $s: $strings")
                    }
                    val name = call.request.queryParameters["name"] ?: "empty"
                    val surname = call.request.queryParameters["surname"] ?: "empty"
                    Name(name, surname)
                }
                contentType.match(ContentType.Text.Plain) -> Name( call.receiveText(), "" )
                else -> Name( "unknown", "type")
            }
            println("-------- receivedName: ${receivedName.name} ${receivedName.surname}")
            val response = "Hello, ${receivedName.name} ${receivedName.surname}!\n"
            val rtnContentType = ContentType.Text.Plain
            call.respondText(response, rtnContentType, status = HttpStatusCode.OK)
        }

        options("/submit_name") {
            println("---- OPTIONS submit_name")
            call.respond(HttpStatusCode.OK)
            /*
            call.request.headers.forEach { s, strings ->
                println("---------- header $s: $strings")
            }
            //val origin = call.request.origin // .origin.orElse("*")
            //if (origin in allowedOrigins || origin.startsWith("<http://localhost>")) {
                call.respondText(
                    "Allowed methods: ${allowedMethods.joinToString(", ")} \n" +
                            "Allowed headers: ${allowedHeaders.joinToString(", ")}",
                    ContentType.Text.Plain,
                    HttpStatusCode.OK
                )
            //} else {
            //    call.respond(HttpStatusCode.Forbidden)
            //}
             */
        }
    }
}

data class Name(val name: String, val surname: String)
Copy code
import React, { useState } from 'react';
import { serverAddress } from '../config';

function SubmitNameForm() {
  const [name, setName] = useState('');
  const [surname, setSurname] = useState('');
  const [response, setResponse] = useState('');
  const [encodedBody, setEncodedBody] = useState('');

  const handleSubmit = async (event) => {
    event.preventDefault();

    try {
      //const bodyData = JSON.stringify({name, surname});
      //const bodyHeaders = "'Content-Type': 'application/json; charset=UTF-8'";
      const bodyData = 'name=' + name + '&surname=' + surname;
      //const bodyHeaders = "'Content-Type': 'text/plain'";
      const bodyHeaders = "'Content-Type': 'application/x-www-form-urlencoded'"
      const url = serverAddress + '/submit_name'
      setEncodedBody(url + '\n' + bodyData);

      const responseData = await fetch(url, {
        method: 'POST',
        headers: {
         bodyHeaders,
         'Access-Control-Allow-Origin': '*',
         'Origin': '<http://localhost:3000>'
        },
        body: bodyData,
        credentials: 'include',
      });

      const textResponse = await responseData.text();
      //const textResponse = await response.json();
      setResponse('Response: ' + textResponse);
    } catch (error) {
      console.error('Error submitting name:', error);
      //setResponse('An error occurred. Please try again.');
      if(error.response) {
            // The request was made and server responded with a status code
            // that falls out of the range of 2xx
            console.log(error.response.data);
            console.log(error.response.status);
            console.log(error.response.headers);
            setResponse('Response code: ' + error.response.status + ', headers: ' + error.response.headers);
      } else if(error.request) {
            // The request was made but no response was received
            console.log(error.request);
            setResponse('No response: ' + error.request);
      } else {
            // Something happened setting up request that triggered an Error
            console.log('Error', error.message);
            setResponse('Request setup: ' + error.message);
      }
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <p>
        <label htmlFor="name">Enter your name:</label>
        <input type="text" id="name" value={name} onChange={(e) => setName(e.target.value)} />
      </p><p>
        <label htmlFor="name">Enter your surname:</label>
        <input type="text" id="surname" value={surname} onChange={(e) => setSurname(e.target.value)} />
      </p>
      <button type="submit">Submit</button>
      <p>{response}</p>
      <p>Body {encodedBody}</p>
    </form>
  );
}

export default SubmitNameForm;
Copy code
// config.js
export const serverAddress = '<http://localhost:8080>';
Ktor version is 2.3.7
a
This setup works for me:
Copy code
install(CORS) {
        anyHost()
        method(HttpMethod.Put)
        method(HttpMethod.Post)
        method(HttpMethod.Options)
        header(HttpHeaders.ContentType)
}
t
That still gives me a 403 Forbidden
But I don't method() and header(). I have allowMethod() and allowHeader(). Is that because of a Ktor version difference?
My Kotlin experience if five years of Android work. I'm still figuring out Ktor and REST
a
Oh, I’m using ktor 1.6.8, sorry, this is why you don’t have method() and header()
Anyway, you can find my implementation here, perhaps it will help you https://github.com/andrewkuryan/brownie-server/
t
Thanks, I'll take a look
a
Does it work with the
anyHost()
method call?
t
No. I have tried anyHost(), allowHost("*"), and allowHost("localhost:3000", schemes = listOf("http"))
a
I am able to allow the POST request from a React application with the following CORS configuration:
Copy code
install(CORS) {
    allowHost("localhost:3000")
}
The request looks like this:
Copy code
fetch("<http://localhost:8000>", {
  method: "POST"
}).then((body) => body.text()).then((t) => console.log(t))
🙌 1
t
I'm pretty much a newbie with React. Is your endpoint in that request "/", or is it specified some other way?
a
Yes, It's
POST /
.
t
Thanks. That worked for me. Now to put back the pieces one at a time. Maybe it won't break!
What I'm seeing is that this doesn't call the preflight OPTIONS, which is where things were failing. And that's fine for my purposes, but I still wonder why OPTIONS was returning forbidden no matter what I did
I got my toy app fully working with GET and POST, so now I'm modifying the real app. Thanks again @Aleksei Tirman [JB]
👍 1