Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

usage example #290

Open
tanishiking opened this issue Oct 26, 2023 · 16 comments
Open

usage example #290

tanishiking opened this issue Oct 26, 2023 · 16 comments

Comments

@tanishiking
Copy link

tanishiking commented Oct 26, 2023

Hi, first of all, thank you for providing this library!

Could someone please share an example of how to use any GCP libraries?

I've been attempting to communicate with Firestore using http4s-grpc-google-cloud-firestore-v1, but I'm encountering an issue where it fails with a java.util.NoSuchElementException before sending the request to Firestore.

context: I'm attempting to develop example app that consumes some GCP application on CloudRun based on ChristopherDavenport/scala-native-ember-example#7

//> using scala 3
//> using dep "org.http4s::http4s-ember-client:0.23.23"
//> using dep "org.typelevel::log4cats-slf4j:2.6.0"
//> using dep "io.chrisdavenport::http4s-grpc-google-cloud-firestore-v1:3.15.2+0.0.6"

import cats.syntax.all._
import cats.effect._
import org.http4s._

import org.typelevel.log4cats.LoggerFactory
import org.typelevel.log4cats.slf4j.Slf4jFactory
import org.http4s.client.Client
import org.http4s.ember.client.EmberClientBuilder

import com.google.firestore.v1.firestore.Firestore
import com.google.firestore.v1.document.Document
import com.google.firestore.v1.firestore.GetDocumentRequest
import com.google.firestore.v1.firestore.GetDocumentRequest.ConsistencySelector

object Main extends IOApp:
  override def run(args: List[String]): IO[ExitCode] =
    val projectId: String = args(0)
    val databaseId: String = args(1)
    createClient().use { client =>
      val firestore = Firestore.fromClient(
        client,
        Uri
          .fromString("https://firestore.googleapis.com")
          .getOrElse(throw new RuntimeException("invalid firestore uri"))
      )
      val docId = "1"
      firestore.getDocument(
        GetDocumentRequest.of(
          name = createDocumentName(projectId, databaseId, "jokes", docId),
          mask = None,
          consistencySelector = ConsistencySelector.Empty
        ),
        Headers.empty
      ).flatMap { doc =>
        IO.println(doc)
      } >> ExitCode.Success.pure[IO]
    }
  
  private implicit val loggerFactory: LoggerFactory[IO] =
    Slf4jFactory.create[IO]

  def createDocumentName(
      projectId: String,
      databaseId: String,
      collectionId: String,
      jokeId: String
  ): String = {
    val documentPath = s"projects/$projectId/databases/$databaseId/documents"
    s"$documentPath/$collectionId/$jokeId"
  }
  
  def createClient(): Resource[IO, Client[IO]] =
    EmberClientBuilder
      .default[IO]
      .withHttp2
      .build
  
  def createFirestoreClient(client: Client[IO]): Firestore[IO] =
    Firestore.fromClient[IO](
      client,
      Uri
        .fromString("https://firestore.googleapis.com")
        .getOrElse(throw new RuntimeException("invalid firestore uri"))
    )
❯ scala-cli run main.scala -- tanishiking ember
Compiling project (Scala 3.3.0, JVM)
Compiled project (Scala 3.3.0, JVM)
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
java.util.NoSuchElementException
        at fs2.Stream$CompileOps.lastOrError$$anonfun$1$$anonfun$1(Stream.scala:4918)
        at scala.Option.fold(Option.scala:263)
        at fs2.Stream$CompileOps.lastOrError$$anonfun$1(Stream.scala:4918)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at modify @ fs2.internal.Scope.close(Scope.scala:262)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at rethrow$extension @ fs2.Compiler$Target.compile$$anonfun$1(Compiler.scala:157)
        at as @ fs2.io.net.SocketGroupCompanionPlatform$AsyncSocketGroup.connect$1$$anonfun$1$$anonfun$1(SocketGroupPlatform.scala:76)
        at get @ org.http4s.ember.core.h2.H2Client$.impl$$anonfun$1(H2Client.scala:343)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at flatMap @ fs2.Pull$.goCloseScope$1$$anonfun$1$$anonfun$3(Pull.scala:1217)
        at update @ org.http4s.ember.core.h2.H2Connection.readLoop$$anonfun$1(H2Connection.scala:549)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
        at flatMap @ fs2.Compiler$Target.flatMap(Compiler.scala:163)
@armanbilge
Copy link
Collaborator

Cool!

but I'm encountering an issue where it fails with a java.util.NoSuchElementException before sending the request to Firestore.

How did you determine that the error occurs before the request is sent? I was thinking the error was originating from here, but that's the response decoder.

https://github.com/davenverse/http4s-grpc/blob/305ea92d0af8bb5ddf9aca377d5292a105c2722b/core/src/main/scala/org/http4s/grpc/codecs/Messages.scala#L20

Is authentication needed to access this API?

@tanishiking
Copy link
Author

tanishiking commented Oct 27, 2023

Nevermind, I was misunderstanding, and it seems we get an error on receive the response 😅
After adding Logger middleware to the client, it says:

HTTP/2.0 POST https://firestore.googleapis.com/google.firestore.v1.Firestore/GetDocument Headers(te: trailers, grpc-encoding: identity, grpc-accept-encoding: identity, Content-Type: application/grpc+proto) body="<
:projects/tanishiking-dev/databases/ember/documents/jokes/1"
HTTP/2.0 404 Not Found Headers(date: Fri, 27 Oct 2023 05:43:51 GMT, content-type: text/html; charset=UTF-8, server: ESF, content-length: 1602, x-xss-protection: 0, x-frame-options: SAMEORIGIN, x-content-type-options: nosniff, alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000) body="<!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 404 (Not Found)!!1</title>
  <style>
    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
  </style>
  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>
  <p><b>404.</b> <ins>That’s an error.</ins>
  <p>The requested URL <code>/google.firestore.v1.Firestore/GetDocument</code> was not found on this server.  <ins>That’s all we know.</ins>

It seems I pointed an wrong baseUri for firestore?


I could retrieve document from the firestore emulator 👍
Gonna figure out how to authenticate both on local environment and CloudRun

@tanishiking
Copy link
Author

tanishiking commented Oct 27, 2023

it seems i need to learn https://googleapis.github.io/HowToRPC.html 🤓

  • We needed to request to the services with headers.Content-Type(new MediaType("application", "application/x-protobuf"))
  • BaseUri should be https://firestore.googleapis.com/$rpc

@tanishiking
Copy link
Author

Here's a progress https://github.com/tanishiking/http4s-firestore now I'm receiving RESOURCE_PROJECT_INVALID 🤔

@tanishiking
Copy link
Author

tanishiking commented Oct 30, 2023

Okay, it worked at least on JVM 🎉

//> using scala 3
//> using dep "org.http4s::http4s-ember-client:0.23.23"
//> using dep "io.chrisdavenport::http4s-grpc-google-cloud-firestore-v1:3.15.2+0.0.6"

import cats.syntax.all._
import cats.effect._
import org.http4s._

import org.http4s.client.Client
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.client.middleware.Logger

import com.google.firestore.v1.firestore.Firestore
import com.google.firestore.v1.document.Document
import com.google.firestore.v1.firestore.GetDocumentRequest
import com.google.firestore.v1.firestore.GetDocumentRequest.ConsistencySelector
import com.google.firestore.v1.firestore.CreateDocumentRequest
import com.google.firestore.v1.document.Value
import com.google.firestore.v1.document.Value.ValueTypeOneof

object Main extends IOApp:
  override def run(args: List[String]): IO[ExitCode] =
    val projectId: String = args(0)
    val accessToken: String = args(1)
    createClient().use { rawClient =>
      val client = Logger[IO](
        logHeaders = true,
        logBody = true,
        redactHeadersWhen = _ => false,
        logAction = Some(msg => IO.println(msg))
      )(rawClient)
      val firestore = Firestore.fromClient(
        client,
        Uri
          .fromString("https://firestore.googleapis.com")
          .getOrElse(throw new RuntimeException("invalid firestore uri"))
      )
      val docId = System.currentTimeMillis().toString
      firestore
        .createDocument(
          CreateDocumentRequest.of(
            parent = s"projects/$projectId/databases/(default)/documents",
            collectionId = "jokes",
            documentId = docId,
            document = Some(Document.of(
              name = "",
              fields = Map(
                "joke" -> Value.of(
                  ValueTypeOneof.StringValue("joke")
                )
              ),
              createTime = None,
              updateTime = None
            )),
            mask = None
          ),
          Headers.of(
            headers.Authorization(
              Credentials.Token(AuthScheme.Bearer, accessToken)
            ),
            headers.`Content-Type`(
              new MediaType("application", "grpc")
            )
          )
        )
        .flatMap { doc =>
          IO.println(doc)
        } >> ExitCode.Success.pure[IO]
    }

  def createClient(): Resource[IO, Client[IO]] =
    EmberClientBuilder
      .default[IO]
      .withHttp2
      .build

scala-cli main.scala -- your_project $(gcloud auth application-default print-access-token)

@armanbilge
Copy link
Collaborator

@tanishiking I was looking more closely at the guide you linked. I'm a little confused, I wonder if you're actually invoking the "fallback" instead of the "true" gRPC API 🤔

https://googleapis.github.io/HowToRPC.html#grpc-fallback-experimental

@armanbilge
Copy link
Collaborator

armanbilge commented Oct 30, 2023

  • We needed to request to the services with headers.Content-Type(new MediaType("application", "application/x-protobuf"))

Unfortunately this also seems incorrect. If you look at the logs for the original request in #290 (comment) you can see it is already including the correct header Content-Type: application/grpc+proto.

@tanishiking
Copy link
Author

I was looking more closely at the guide you linked. I'm a little confused, I wonder if you're actually invoking the "fallback" instead of the "true" gRPC API 🤔

Yeah, I was also thinking about it, but I'm not sure how can we force to use "true" gRPC API 🤔

Unfortunately this also seems incorrect. If you look at the logs for the original request in #290 (comment) you can see it is already including the correct header Content-Type: application/grpc+proto.

Yes, that statement turned out to be incorrect. However, we still need to set ContentType as application/grpc or application/x-protobuf it seems. Otherwise (with default Content-Type: application/grpc+proto) we get <p><b>404.</b> <ins>That’s an error.</ins><p>The requested URL <code>/$rpc/google.firestore.v1.Firestore/CreateDocument</code> was not found on this server. <ins>That’s all we know.</ins>

@tanishiking
Copy link
Author

It seems like the fallback protocol is like https://github.com/googleapis/googleapis.github.io/blob/bcb-to-fb/examples/rpc/rust/src/main.rs that RPC using normal HTTP POST

    let mut res = client.post("https://language.googleapis.com/$rpc/google.cloud.language.v1.LanguageService/AnalyzeEntities")
		.header(ContentType(Mime::from_str("application/x-protobuf").unwrap()))
		.header(XGoogApiKey(google_api_key.to_owned()))
		.body(serialized_bytes)
		.send().unwrap();

So, if http4s-grpc doesn't fallback to the HTTP call, we're using gRPC on the above example maybe? (I'm quite new to gRPC and not sure how can we confirm we're communicating over gRPC 🙇)

❯ scala-cli main.scala -- tanishiking-dev $(gcloud auth application-default print-access-token)
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
HTTP/2.0 POST https://firestore.googleapis.com/$rpc/google.firestore.v1.Firestore/CreateDocument Headers(te: trailers, grpc-encoding: identity, grpc-accept-encoding: identity, Authorization: Bearer ..., Content-Type: application/grpc) body="a
1698678199810"shiking-dev/databases/(default)/documentsjokes
joke�joke"
HTTP/2.0 200 OK Headers(content-disposition: attachment, content-type: application/grpc, date: Mon, 30 Oct 2023 15:03:20 GMT, alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000) body="y
Jprojects/tanishiking-dev/databases/(default)/documents/jokes/1698678199810
joke�joke
        ����ع��"
               ����ع��"
Document(projects/tanishiking-dev/databases/(default)/documents/jokes/1698678199810,Map(joke -> Value(StringValue(joke),UnknownFieldSet(Map()))),Some(Timestamp(1698678200,524279000,UnknownFieldSet(Map()))),Some(Timestamp(1698678200,524279000,UnknownFieldSet(Map()))),UnknownFieldSet(Map()))

@armanbilge
Copy link
Collaborator

armanbilge commented Oct 30, 2023

Well, it's complicated, because gRPC itself is defined over HTTP/2 😅 So I'm not entirely sure how the fallback is different from that.


I believe the Content-Type: application/grpc+proto is required by the gRPC specification. So if you are not using that, probably you are not actually using gRPC?

https://github.com/grpc/grpc/blob/c2f49c2d3b236bf04f9da786c6b5b412271df7dc/doc/PROTOCOL-HTTP2.md#example


Otherwise (with default Content-Type: application/grpc+proto) we get <p><b>404.</b> <ins>That’s an error.</ins><p>The requested URL <code>/$rpc/google.firestore.v1.Firestore/CreateDocument</code> was not found on this server. <ins>That’s all we know.</ins>

I'm very suspicious about this /$rpc/ thing, I do not think that it is the true gRPC endpoint which is why it does not accept the correctly-formed request.

For most Google APIs, the BaseUrl looks like https://language.googleapis.com/$rpc (experimental: this format may change in the future).

It seems strange if true gRPC is using an experimental URL format. But it would make sense for "gRPC Fallback (Experimental)" to be use an experimental URL format.


Sorry, I'm mostly throwing mud at your code without answers of my own 😅 Thanks for your efforts to actually make this stuff work 🙏

We may need to study an official Google library to see how its invoking the gRPCs.

@tanishiking
Copy link
Author

I believe the Content-Type: application/grpc+proto is required by the gRPC specification. So if you are not using that, probably you are not actually using gRPC?
It seems strange if true gRPC is using an experimental URL format. But it would make sense for "gRPC Fallback (Experimental)" to be use an experimental URL format.

Hm, that makes sense.

Anyway, it seems we need to study from an official Google library as you mentioned 😅

Sorry, I'm mostly throwing mud at your code without answers of my own

No worries! I really appreciate having someone to talk to about this issue 😄


BTW, I made an another sloppy example that programmatically authenticate GCP using service account key (instead of gcloud auth application-default print-access-token) https://github.com/tanishiking/http4s-firestore-auth

@armanbilge
Copy link
Collaborator

programmatically authenticate GCP using service account key

Nice! I have a JVM/JS implementation of this in one of my other projects:

https://github.com/armanbilge/gcp4s/blob/2ac68bbf2f06e749205566fbf8690dc276ed1d26/core/shared/src/main/scala/gcp4s/auth/GoogleOAuth2.scala#L42

I started working on porting this to a new googleapis-http4s-runtime library. I'm refreshing the code and adding Native support.

https://github.com/davenverse/googleapis-http4s-runtime

@tanishiking
Copy link
Author

Awesome! Are you planning to deprecate the gcp4s for googleapis-http4s-runtime?

@armanbilge
Copy link
Collaborator

Are you planning to deprecate the gcp4s

Currently that library has a couple other modules in it that I haven't figured out what to do with yet. For example it supports BigQuery APIs that are not available over gRPC.

@tanishiking tanishiking changed the title Can I get any usage example of this library? usage example Dec 8, 2023
@i10416
Copy link

i10416 commented Jan 8, 2024

@i10416
Copy link

i10416 commented Jan 9, 2024

Example with googleapis-runtime:

//> using scala "2.13.12"
//> using repositories "sonatype-s01:snapshots"
//> using dep "dev.i10416::http4s-googleapis-runtime:0.0-20544fa-SNAPSHOT"
//> using dep "org.http4s::http4s-core:0.23.25"
//> using dep "org.http4s::http4s-ember-client:0.23.25"
//> using dep "org.typelevel::cats-effect:3.5.2"
//> using dep "io.chrisdavenport::http4s-grpc-google-cloud-firestore-v1:3.15.2+0.0.6"
import org.http4s.syntax.all._
import com.google.firestore.v1.firestore.Firestore
import com.google.firestore.v1.document.Document
import com.google.firestore.v1.firestore.GetDocumentRequest
import com.google.firestore.v1.firestore.GetDocumentRequest.ConsistencySelector
import com.google.firestore.v1.firestore.CreateDocumentRequest
import com.google.firestore.v1.document.Value
import com.google.firestore.v1.document.Value.ValueTypeOneof

import cats.effect._
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.googleapis.runtime.auth.ApplicationDefaultCredentials
import com.google.firestore.v1.firestore.ListDocumentsRequest
import org.http4s.Headers
import org.http4s.headers.`Content-Type`
import org.http4s.MediaType
import org.http4s.headers.Authorization
import org.http4s.Credentials
import org.http4s.AuthScheme

object Main extends IOApp {
  private val projectId: String = ??? // your GCP project id
  private val parentCollection: String = ???
  private val collectionId: String =  ???
  def run(args: List[String]): IO[ExitCode] = EmberClientBuilder
    .default[IO]
    .withHttp2
    .build
    .use { client =>
      for {
        oauth2 <- ApplicationDefaultCredentials(client)
        tkn <- oauth2.get
        f = Firestore.fromClient(client, uri"https://firestore.googleapis.com")
        response <- f.listDocuments(
          ListDocumentsRequest(
            parent =
              s"projects/$projectId/databases/(default)/documents/$parentCollection",
            collectionId = collectionId,
            pageSize = 10
          ),
          Headers(
            Authorization(
              Credentials.Token(AuthScheme.Bearer, tkn.token)
            ),
            `Content-Type`(
              new MediaType("application", "grpc")
            )
          )
        )
        _ <- IO.println(response)
      } yield ()

    }
    .handleErrorWith(IO.println(_))
    .as(ExitCode.Success)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants