Documentation needed: Low-level HTTP-client from Micronaut needs to be in a Micronaut bean in order to be injected
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
### Task List
- [x] Steps to reproduce provided
- [x] Stacktrace (if present) provided
- [x] Example that reproduces the problem uploaded to Github
- [x] Full description of the issue provided (see below)
### Steps to Reproduce
1. Create a new Grails 4 project: curl -O start.grails.org/test-micronaut-http.zip -d version=4.0.0 -d profile=rest-api
2. Create a new controller with a low-level HTTP-client from Micronaut:
```
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.annotation.Client
import javax.inject.Inject
class LowLevelClientController {
@Inject
@Client('http://www.google.com')
RxHttpClient rxHttpClient
def index() {
render rxHttpClient.retrieve('/search?q=hello').blockingSingle()
}
}
```
3. Start the application: `./gradlew bootRun`
4. Call the controller: `curl -i http://localhost:8080/lowLevelClient`
### Expected Behaviour
The rxHttpClient bean should be injected and the call should not fail.
### Actual Behaviour
The rxHttpClient bean could not be injected and the call fails with this stacktrace:
```
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'test.micronaut.http.LowLevelClientController': Unsatisfied dependency expressed through field 'rxHttpClient'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'io.micronaut.http.client.RxHttpClient' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@javax.inject.Inject(), @io.micronaut.http.client.annotation.Client(path=, id=, configuration=class io.micronaut.http.client.HttpClientConfiguration, value=http://www.google.com, errorType=class io.micronaut.http.hateoas.JsonError)}
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:596)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1411)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:592)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1105)
at org.grails.web.mapping.mvc.UrlMappingsInfoHandlerAdapter.handle(UrlMappingsInfoHandlerAdapter.groovy:73)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at org.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:77)
at org.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:67)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
```
### Environment Information
- **Operating System**: Ubuntu 18.04
- **Grails Version:** 4.0.0
- **JDK Version:** 8.0.222-zulu
### Example Application
https://github.com/andersaaberg/test-micronaut-http/tree/master
### Analysis
I had a talk with James Kleeh on slack, who explained that @Client is a custom scope and that annotation is not visible to Micronaut because its not being injected into a Micronaut bean. He did not think that the issue can be solved. So I have made this ticket just to document the following workaround and I hope that someone will include it in the Grails documentation.
### Workaround to inject low-level HTTP-client from Micronaut in Grails 4:
1. Add a new Micronaut bean (that contains the RxHttpClient) in /src/main/groovy/...:
```
import javax.inject.Inject
import javax.inject.Singleton
import groovy.transform.CompileStatic
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.annotation.Client
@CompileStatic
@Singleton
class LowLevelClient {
@Client('http://www.google.com')
@Inject
RxHttpClient rxHttpClient
}
```
2. Inject the rxHttpClient in the controller using the Micronaut bean (LowLevelClient):
```
import javax.inject.Inject
class LowLevelClientController {
@Inject
LowLevelClient lowLevelClient
def index() {
render lowLevelClient.rxHttpClient.retrieve('/search?q=hello').blockingSingle()
}
}
```
The workaround is in the example project branch called "workaround": https://github.com/andersaaberg/test-micronaut-http/tree/workaround
### Workaround to manually instantiate low-level HTTP-client from Micronaut in Grails 4, but still get configuration from application.yml (but possible missing other features):
It is also possible to manually instantiate the low-level HTTP-client from Micronaut in Grails 4, but then it does not load `micronaut.http.client.*` configuration from application.yml. This can be added by injecting the DefaultHttpClientConfiguration bean and then adding it to the DefaultHttpClient constructor. **However, this workaround is missing all other injections than DefaultHttpClientConfiguration in the rxHttpClient, so other rxHttpClient features might not work with this workaround. Hence the workaround above is preferred.**
Example:
```
import io.micronaut.http.client.DefaultHttpClient
import io.micronaut.http.client.DefaultHttpClientConfiguration
import io.micronaut.http.client.RxHttpClient
import javax.annotation.PostConstruct
import javax.inject.Inject
class LowLevelClientController {
@Inject
DefaultHttpClientConfiguration defaultHttpClientConfiguration
RxHttpClient rxHttpClient
@PostConstruct
void initHttpClient() {
rxHttpClient = new DefaultHttpClient(new URL('http://google.com'), defaultHttpClientConfiguration)
}
def index() {
render rxHttpClient.retrieve('/search?q=hello').blockingSingle()
}
}
```
The workaround is in the example project branch called "workaround2": https://github.com/andersaaberg/test-micronaut-http/tree/workaround2
Contributor guide
Research direction
Start with the issue's Grails 4 reproduction and the example application's workaround and workaround2 branches. Document the preferred Micronaut bean approach for injecting RxHttpClient, while noting the limitations of manual DefaultHttpClient construction. Done means Grails documentation explains the failure, preferred workaround, and configuration caveat.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy
- Domain
- backend, documentation
- Issue type
- Documentation
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100