Skip to content

Commit 48d82a9

Browse files
authored
Fix the pending problem when requesting RSS endpoint (#64)
This PR fixes the problem and refine the logic of getting file bytes. After upgrading to Halo 2.20.16, requesting RSS endpoint may cause entirely pending issue. Please see the following concrete logs: ```java 2025-03-11T01:04:39.536+08:00 ERROR 7 --- [loomBoundedElastic-361] a.w.r.e.AbstractErrorWebExceptionHandler : [756c052a-61] 500 Server Error for HTTP GET "/rss.xml" com.google.common.util.concurrent.UncheckedExecutionException: reactor.core.Exceptions$ReactiveException: java.lang.InterruptedException at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2085) ~[guava-33.3.1-jre.jar:na] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ run.halo.app.security.InitializeRedirectionWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.app.infra.webfilter.LocaleChangeWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.app.security.device.DeviceSessionFilter [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.cache.page.PageCacheWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ SecurityWebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ AuthorizationWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ ExceptionTranslationWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ LogoutWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ ServerRequestCacheWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ SecurityContextServerWebExchangeWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ SecurityWebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ AnonymousAuthenticationWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ SecurityWebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.social.login.security.SocialAuthenticatorFilter [DefaultWebFilterChain] *__checkpoint ⇢ SecurityWebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ AuthenticationWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ AuthenticationWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ AuthenticationWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ MapOAuth2AuthenticationFilter [DefaultWebFilterChain] *__checkpoint ⇢ AuthenticationWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ SecurityWebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.social.login.security.SocialAuthorizationRequestRedirectWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ SecurityWebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ ReactorContextWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ CsrfWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ CorsWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ HttpHeaderWriterWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ SecurityWebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.seo.tools.AdvancedRedirectWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.moments.rss.OldRssRouteRedirectionFilter [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.comment.widget.captcha.CommentCaptchaFilter [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.seo.tools.CrawlRecordFilter [DefaultWebFilterChain] *__checkpoint ⇢ cc.ryanc.staticpages.endpoint.RewriteOnNotFoundFilter [DefaultWebFilterChain] *__checkpoint ⇢ run.halo.app.infra.webfilter.AdditionalWebFilterChainProxy [DefaultWebFilterChain] *__checkpoint ⇢ org.springframework.web.filter.reactive.ServerWebExchangeContextFilter [DefaultWebFilterChain] *__checkpoint ⇢ HTTP GET "/rss.xml" [ExceptionHandlingWebHandler] Original Stack Trace: at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2085) ~[guava-33.3.1-jre.jar:na] at com.google.common.cache.LocalCache.get(LocalCache.java:4017) ~[guava-33.3.1-jre.jar:na] at com.google.common.cache.LocalCache$LocalManualCache.get(LocalCache.java:4898) ~[guava-33.3.1-jre.jar:na] at run.halo.feed.RssCacheManager.lambda$get$2(RssCacheManager.java:29) ~[na:na] at reactor.core.publisher.MonoCallable$MonoCallableSubscription.request(MonoCallable.java:137) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.MonoCacheTime$CoordinatorSubscriber.onSubscribe(MonoCacheTime.java:293) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.MonoCallable.subscribe(MonoCallable.java:48) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.MonoCacheTime.subscribeOrReturn(MonoCacheTime.java:143) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.Mono.subscribe(Mono.java:4560) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.MonoSubscribeOn$SubscribeOnSubscriber.run(MonoSubscribeOn.java:126) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler$SchedulerTask.run(BoundedElasticThreadPerTaskScheduler.java:1013) ~[reactor-core-3.7.3.jar:3.7.3] at java.base/java.lang.VirtualThread.run(Unknown Source) ~[na:na] Caused by: reactor.core.Exceptions$ReactiveException: java.lang.InterruptedException at reactor.core.Exceptions$ReactiveException.fillInStackTrace(Exceptions.java:736) ~[reactor-core-3.7.3.jar:3.7.3] at java.base/java.lang.Throwable.<init>(Unknown Source) ~[na:na] at java.base/java.lang.Throwable.<init>(Unknown Source) ~[na:na] at java.base/java.lang.Exception.<init>(Unknown Source) ~[na:na] at java.base/java.lang.RuntimeException.<init>(Unknown Source) ~[na:na] at reactor.core.Exceptions$ReactiveException.<init>(Exceptions.java:726) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.Exceptions.propagate(Exceptions.java:410) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:96) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.Mono.block(Mono.java:1779) ~[reactor-core-3.7.3.jar:3.7.3] at run.halo.feed.RssCacheManager.lambda$get$1(RssCacheManager.java:32) ~[na:na] at com.google.common.cache.LocalCache$LocalManualCache$1.load(LocalCache.java:4903) ~[guava-33.3.1-jre.jar:na] at com.google.common.cache.LocalCache$LoadingValueReference.loadFuture(LocalCache.java:3574) ~[guava-33.3.1-jre.jar:na] at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2316) ~[guava-33.3.1-jre.jar:na] at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2189) ~[guava-33.3.1-jre.jar:na] at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2079) ~[guava-33.3.1-jre.jar:na] at com.google.common.cache.LocalCache.get(LocalCache.java:4017) ~[guava-33.3.1-jre.jar:na] at com.google.common.cache.LocalCache$LocalManualCache.get(LocalCache.java:4898) ~[guava-33.3.1-jre.jar:na] at run.halo.feed.RssCacheManager.lambda$get$2(RssCacheManager.java:29) ~[na:na] at reactor.core.publisher.MonoCallable$MonoCallableSubscription.request(MonoCallable.java:137) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.MonoCacheTime$CoordinatorSubscriber.onSubscribe(MonoCacheTime.java:293) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.MonoCallable.subscribe(MonoCallable.java:48) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.MonoCacheTime.subscribeOrReturn(MonoCacheTime.java:143) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.Mono.subscribe(Mono.java:4560) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.publisher.MonoSubscribeOn$SubscribeOnSubscriber.run(MonoSubscribeOn.java:126) ~[reactor-core-3.7.3.jar:3.7.3] at reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler$SchedulerTask.run(BoundedElasticThreadPerTaskScheduler.java:1013) ~[reactor-core-3.7.3.jar:3.7.3] at java.base/java.lang.VirtualThread.run(Unknown Source) ~[na:na] Caused by: java.lang.InterruptedException: null at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(Unknown Source) ~[na:na] at java.base/java.util.concurrent.CountDownLatch.await(Unknown Source) ~[na:na] at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:91) ~[reactor-core-3.7.3.jar:3.7.3] ... 18 common frames omitted ``` /kind bug ```release-note None ```
1 parent 9d06473 commit 48d82a9

7 files changed

Lines changed: 113 additions & 37 deletions

File tree

app/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ jar {
1515
dependencies {
1616
implementation project(':api')
1717
compileOnly 'run.halo.app:api'
18-
implementation 'org.dom4j:dom4j:2.1.3'
18+
implementation 'org.dom4j:dom4j:2.1.4'
1919
implementation('org.apache.commons:commons-text:1.10.0') {
2020
// Because the transitive dependency already exists in halo.
2121
exclude group: 'org.apache.commons', module: 'commons-lang3'
2222
}
2323

2424
testImplementation 'run.halo.app:api'
2525
testImplementation 'org.springframework.boot:spring-boot-starter-test'
26+
testImplementation 'io.projectreactor:reactor-test'
2627
}
2728

2829
tasks.register('copyUI', Copy) {

app/src/main/java/run/halo/feed/RssCacheManager.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.google.common.cache.Cache;
44
import com.google.common.cache.CacheBuilder;
55
import java.time.Duration;
6+
import java.util.concurrent.ExecutionException;
67
import java.util.function.Predicate;
78
import lombok.RequiredArgsConstructor;
89
import org.springframework.context.event.EventListener;
@@ -18,21 +19,20 @@
1819
@RequiredArgsConstructor
1920
public class RssCacheManager {
2021
private final Duration expireMinutes = Duration.ofMinutes(60);
21-
private final Cache<String, String> cache = CacheBuilder.newBuilder()
22+
private final Cache<String, Mono<String>> cache = CacheBuilder.newBuilder()
2223
.expireAfterWrite(expireMinutes)
2324
.build();
2425

2526
private final SystemInfoGetter systemInfoGetter;
2627
private final ReactiveSettingFetcher settingFetcher;
2728

2829
public Mono<String> get(String requestPath, Mono<RSS2> loader) {
29-
return Mono.fromCallable(() -> cache.get(requestPath,
30-
() -> generateRssXml(requestPath, loader)
31-
.doOnNext(xml -> cache.put(requestPath, xml))
32-
.block()
33-
))
34-
.cache()
35-
.subscribeOn(Schedulers.boundedElastic());
30+
try {
31+
return cache.get(requestPath, () -> generateRssXml(requestPath, loader).cache())
32+
.subscribeOn(Schedulers.boundedElastic());
33+
} catch (ExecutionException e) {
34+
return Mono.error(e);
35+
}
3636
}
3737

3838
private Mono<String> generateRssXml(String requestPath, Mono<RSS2> loader) {
@@ -51,10 +51,8 @@ private Mono<String> generateRssXml(String requestPath, Mono<RSS2> loader) {
5151
.doOnNext(prop -> builder.withExtractRssTags(prop.getRssExtraTags()));
5252

5353
return Mono.when(rssMono, generatorMono, extractTagsMono)
54-
// toXmlString is a blocking operation
55-
.then(Mono.fromCallable(builder::toXmlString)
56-
.subscribeOn(Schedulers.boundedElastic())
57-
);
54+
.thenReturn(builder)
55+
.flatMap(RssXmlBuilder::toXmlString);
5856
}
5957

6058
@EventListener(PluginConfigUpdatedEvent.class)

app/src/main/java/run/halo/feed/RssXmlBuilder.java

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.google.common.base.Throwables;
44
import java.io.StringReader;
5+
import java.net.URI;
56
import java.nio.charset.StandardCharsets;
67
import java.time.Duration;
78
import java.time.Instant;
@@ -20,10 +21,14 @@
2021
import org.springframework.http.HttpHeaders;
2122
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
2223
import org.springframework.lang.NonNull;
24+
import org.springframework.lang.Nullable;
2325
import org.springframework.util.CollectionUtils;
2426
import org.springframework.web.reactive.function.client.WebClient;
27+
import org.springframework.web.reactive.function.client.WebClientException;
2528
import org.springframework.web.util.UriComponentsBuilder;
2629
import org.springframework.web.util.UriUtils;
30+
import reactor.core.publisher.Flux;
31+
import reactor.core.publisher.Mono;
2732
import reactor.netty.http.client.HttpClient;
2833
import run.halo.feed.telemetry.TelemetryEndpoint;
2934

@@ -82,7 +87,7 @@ RssXmlBuilder withExternalUrl(String externalUrl) {
8287
return this;
8388
}
8489

85-
public String toXmlString() {
90+
public Mono<String> toXmlString() {
8691
Document document = DocumentHelper.createDocument();
8792

8893
Element root = DocumentHelper.createElement("rss");
@@ -136,9 +141,10 @@ public String toXmlString() {
136141
}
137142

138143
var items = rss2.getItems();
139-
createItemElementsToChannel(channel, items);
140144

141-
return document.asXML();
145+
return createItemElementsToChannel(channel, items)
146+
.thenReturn(document)
147+
.map(Document::asXML);
142148
}
143149

144150
private void copyAttributesAndChildren(Element target, Element source) {
@@ -171,14 +177,16 @@ private Element parseXmlString(String xml) throws DocumentException {
171177
}
172178
}
173179

174-
private void createItemElementsToChannel(Element channel, List<RSS2.Item> items) {
180+
private Mono<Void> createItemElementsToChannel(Element channel, List<RSS2.Item> items) {
175181
if (CollectionUtils.isEmpty(items)) {
176-
return;
182+
return Mono.empty();
177183
}
178-
items.forEach(item -> createItemElementToChannel(channel, item));
184+
return Flux.fromIterable(items)
185+
.flatMap(item -> createItemElementToChannel(channel, item))
186+
.then();
179187
}
180188

181-
private void createItemElementToChannel(Element channel, RSS2.Item item) {
189+
private Mono<Void> createItemElementToChannel(Element channel, RSS2.Item item) {
182190
Element itemElement = channel.addElement("item");
183191
itemElement.addElement("title")
184192
.addCDATA(XmlCharUtils.removeInvalidXmlChar(item.getTitle()));
@@ -205,18 +213,19 @@ private void createItemElementToChannel(Element channel, RSS2.Item item) {
205213
.addText(item.getAuthor());
206214
}
207215

216+
Mono<Void> handleEnclosure = Mono.empty();
208217
if (StringUtils.isNotBlank(item.getEnclosureUrl())) {
209218
var enclosureElement = itemElement.addElement("enclosure")
210219
.addAttribute("url", item.getEnclosureUrl())
211220
.addAttribute("type", item.getEnclosureType());
212-
213221
var enclosureLength = item.getEnclosureLength();
222+
enclosureElement.addAttribute("length", enclosureLength);
214223
if (StringUtils.isBlank(enclosureLength)) {
215224
// https://www.rssboard.org/rss-validator/docs/error/MissingAttribute.html
216-
var fileBytes = getFileSizeBytes(item.getEnclosureUrl());
217-
enclosureLength = String.valueOf(fileBytes);
225+
handleEnclosure = getFileSizeBytes(item.getEnclosureUrl())
226+
.doOnNext(fileBytes -> enclosureElement.addAttribute("length", String.valueOf(fileBytes)))
227+
.then();
218228
}
219-
enclosureElement.addAttribute("length", enclosureLength);
220229
}
221230

222231
nullSafeList(item.getCategories())
@@ -264,6 +273,7 @@ private void createItemElementToChannel(Element channel, RSS2.Item item) {
264273
}
265274
}
266275
});
276+
return handleEnclosure;
267277
}
268278

269279
private String getDescriptionWithTelemetry(RSS2.Item item) {
@@ -298,23 +308,47 @@ static String instantToString(Instant instant) {
298308
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
299309
}
300310

311+
301312
@NonNull
302-
private Long getFileSizeBytes(String url) {
313+
Mono<Long> getFileSizeBytes(String url) {
303314
return webClient.get()
304-
.uri(url)
315+
.uri(URI.create(url))
305316
.header(HttpHeaders.USER_AGENT, UA)
306317
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
307318
.header(HttpHeaders.RANGE, "bytes=0-0")
319+
.headers(headers -> {
320+
if (StringUtils.isNotBlank(externalUrl)) {
321+
// For Referrer anti-hotlinking
322+
headers.set(HttpHeaders.REFERER, externalUrl);
323+
}
324+
})
308325
.retrieve()
309326
.toBodilessEntity()
310327
.map(HttpEntity::getHeaders)
311-
.mapNotNull(headers -> headers.getFirst(HttpHeaders.CONTENT_LENGTH))
312-
.map(Long::parseLong)
328+
.mapNotNull(headers -> headers.getFirst(HttpHeaders.CONTENT_RANGE))
329+
.mapNotNull(RssXmlBuilder::parseLengthOfContentRange)
313330
.doOnError(e -> log.debug("Failed to get file size from url: {}", url,
314331
Throwables.getRootCause(e))
315332
)
316333
.onErrorReturn(0L)
317-
.blockOptional()
318-
.orElse(0L);
334+
.defaultIfEmpty(0L);
319335
}
336+
337+
@Nullable
338+
static Long parseLengthOfContentRange(@Nullable String contentRange) {
339+
if (contentRange == null) {
340+
return null;
341+
}
342+
// Refer to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range#syntax
343+
var range = contentRange.split("/");
344+
if (range.length != 2) {
345+
return null;
346+
}
347+
var lengthStr = range[1];
348+
if ("*".equals(lengthStr)) {
349+
return null;
350+
}
351+
return Long.parseLong(range[1]);
352+
}
353+
320354
}

app/src/main/java/run/halo/feed/provider/AbstractPostRssProvider.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ protected Flux<PostWithContent> listPostsByFunc(Function<BasicProp, Flux<Post>>
126126

127127
protected Mono<User> fetchUser(String username) {
128128
return client.fetch(User.class, username)
129-
.switchIfEmpty(Mono.error(new ServerWebInputException("User not found")))
130-
.subscribeOn(Schedulers.boundedElastic());
129+
.switchIfEmpty(Mono.error(new ServerWebInputException("User not found")));
131130
}
132131

133132
private Flux<String> fetchCategoryDisplayName(List<String> categoryNames) {
@@ -137,8 +136,7 @@ private Flux<String> fetchCategoryDisplayName(List<String> categoryNames) {
137136
return client.listAll(Category.class, ListOptions.builder()
138137
.fieldQuery(QueryFactory.in("metadata.name", categoryNames))
139138
.build(), ExtensionUtil.defaultSort())
140-
.map(category -> category.getSpec().getDisplayName())
141-
.subscribeOn(Schedulers.boundedElastic());
139+
.map(category -> category.getSpec().getDisplayName());
142140
}
143141

144142
@Data

app/src/test/java/run/halo/feed/RSS2Test.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Arrays;
77
import java.util.Collections;
88
import org.junit.jupiter.api.Test;
9+
import reactor.test.StepVerifier;
910

1011
class RSS2Test {
1112

@@ -80,7 +81,10 @@ void toXmlString() {
8081
</channel>
8182
</rss>
8283
""".formatted(lastBuildDate);
83-
assertThat(rssXml).isEqualToIgnoringWhitespace(expected);
84+
85+
StepVerifier.create(rssXml)
86+
.assertNext(xml -> assertThat(xml).isEqualToIgnoringWhitespace(expected))
87+
.verifyComplete();
8488
}
8589

8690
@Test
@@ -132,7 +136,9 @@ void extractRssTagsTest() {
132136
</channel>
133137
</rss>
134138
""".formatted(lastBuildDate);
135-
assertThat(rssXml).isEqualToIgnoringWhitespace(expected);
139+
StepVerifier.create(rssXml)
140+
.assertNext(xml -> assertThat(xml).isEqualToIgnoringWhitespace(expected))
141+
.verifyComplete();
136142
}
137143

138144
@Test
@@ -188,6 +194,8 @@ void invalidCharTest() {
188194
</channel>
189195
</rss>
190196
""".formatted(lastBuildDate);
191-
assertThat(rssXml).isEqualToIgnoringWhitespace(expected);
197+
StepVerifier.create(rssXml)
198+
.assertNext(xml -> assertThat(xml).isEqualToIgnoringWhitespace(expected))
199+
.verifyComplete();
192200
}
193201
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package run.halo.feed;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.params.provider.Arguments.arguments;
5+
import static run.halo.feed.RssXmlBuilder.parseLengthOfContentRange;
6+
7+
import java.util.stream.Stream;
8+
import org.junit.jupiter.params.ParameterizedTest;
9+
import org.junit.jupiter.params.provider.Arguments;
10+
import org.junit.jupiter.params.provider.MethodSource;
11+
12+
class RssXmlBuilderTest {
13+
14+
static Stream<Arguments> testParseLengthOfContentRange() {
15+
return Stream.of(
16+
arguments("bytes 0-0/123", 123L),
17+
arguments("bytes */567", 567L),
18+
arguments("bytes 0-0", null),
19+
arguments("bytes 0-0/*", null)
20+
);
21+
}
22+
23+
@ParameterizedTest
24+
@MethodSource
25+
void testParseLengthOfContentRange(String contentRange, Long expected) {
26+
assertEquals(expected, parseLengthOfContentRange(contentRange));
27+
}
28+
}

app/src/test/resources/logback.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<configuration>
3+
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
4+
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
5+
<root level="INFO">
6+
<appender-ref ref="CONSOLE" />
7+
</root>
8+
<logger name="run.halo.feed" level="DEBUG"/>
9+
</configuration>

0 commit comments

Comments
 (0)