INT-2995: Fix HttpHeaders inconsistency
Previously, 'If-Modified-Since' and 'If-Unmodified-Since' HTTP headers were incorrectly processed within from/to HTTP headers mapping, because Spring-Web `HttpHeaders` has a confusing method name, see: https://jira.springsource.org/browse/SPR-10600 * fix 'If-Modified-Since' and 'If-Unmodified-Since' processing independently from `HttpHeaders` * add fallback to formatted string for date ware HTTP headers * add Spring Integration HTTP proxy scenario test JIRA: https://jira.springsource.org/browse/INT-2995
This commit is contained in:
committed by
Gary Russell
parent
9285867465
commit
4609b861d1
@@ -19,7 +19,10 @@ package org.springframework.integration.http.support;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.DateFormat;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -29,9 +32,12 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
@@ -240,6 +246,14 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
|
||||
public static final String HTTP_RESPONSE_HEADER_NAME_PATTERN = "HTTP_RESPONSE_HEADERS";
|
||||
|
||||
// Copy of 'org.springframework.http.HttpHeaders#DATE_FORMATS'
|
||||
private static final String[] DATE_FORMATS = new String[] {
|
||||
"EEE, dd MMM yyyy HH:mm:ss zzz",
|
||||
"EEE, dd-MMM-yy HH:mm:ss zzz",
|
||||
"EEE MMM dd HH:mm:ss yyyy"
|
||||
};
|
||||
|
||||
private static TimeZone GMT = TimeZone.getTimeZone("GMT");
|
||||
|
||||
private volatile String[] outboundHeaderNames = new String[0];
|
||||
|
||||
@@ -637,7 +651,12 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
target.setDate(((Number) value).longValue());
|
||||
}
|
||||
else if (value instanceof String) {
|
||||
target.setDate(Long.parseLong((String) value));
|
||||
try {
|
||||
target.setDate(Long.parseLong((String) value));
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
target.setDate(this.getFirstDate((String) value, DATE));
|
||||
}
|
||||
}
|
||||
else {
|
||||
Class<?> clazz = (value != null) ? value.getClass() : null;
|
||||
@@ -663,7 +682,12 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
target.setExpires(((Number) value).longValue());
|
||||
}
|
||||
else if (value instanceof String) {
|
||||
target.setExpires(Long.parseLong((String) value));
|
||||
try {
|
||||
target.setExpires(Long.parseLong((String) value));
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
target.setExpires(this.getFirstDate((String) value, EXPIRES));
|
||||
}
|
||||
}
|
||||
else {
|
||||
Class<?> clazz = (value != null) ? value.getClass() : null;
|
||||
@@ -679,7 +703,12 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
target.setIfModifiedSince(((Number) value).longValue());
|
||||
}
|
||||
else if (value instanceof String) {
|
||||
target.setIfModifiedSince(Long.parseLong((String) value));
|
||||
try {
|
||||
target.setIfModifiedSince(Long.parseLong((String) value));
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
target.setIfModifiedSince(this.getFirstDate((String) value, IF_MODIFIED_SINCE));
|
||||
}
|
||||
}
|
||||
else {
|
||||
Class<?> clazz = (value != null) ? value.getClass() : null;
|
||||
@@ -687,6 +716,30 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
"Expected Date, Number, or String value for 'If-Modified-Since' header value, but received: " + clazz);
|
||||
}
|
||||
}
|
||||
else if (IF_UNMODIFIED_SINCE.equalsIgnoreCase(name)) {
|
||||
String ifUnmodifiedSinceValue = null;
|
||||
if (value instanceof Date) {
|
||||
ifUnmodifiedSinceValue = this.formatDate(((Date) value).getTime());
|
||||
}
|
||||
else if (value instanceof Number) {
|
||||
ifUnmodifiedSinceValue = this.formatDate(((Number) value).longValue());
|
||||
}
|
||||
else if (value instanceof String) {
|
||||
try {
|
||||
ifUnmodifiedSinceValue = this.formatDate(Long.parseLong((String) value));
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
long longValue = this.getFirstDate((String) value, IF_UNMODIFIED_SINCE);
|
||||
ifUnmodifiedSinceValue = this.formatDate(longValue);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Class<?> clazz = (value != null) ? value.getClass() : null;
|
||||
throw new IllegalArgumentException(
|
||||
"Expected Date, Number, or String value for 'If-Unmodified-Since' header value, but received: " + clazz);
|
||||
}
|
||||
target.set(IF_UNMODIFIED_SINCE, ifUnmodifiedSinceValue);
|
||||
}
|
||||
else if (IF_NONE_MATCH.equalsIgnoreCase(name)) {
|
||||
if (value instanceof String) {
|
||||
target.setIfNoneMatch((String) value);
|
||||
@@ -721,7 +774,12 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
target.setLastModified(((Number) value).longValue());
|
||||
}
|
||||
else if (value instanceof String) {
|
||||
target.setLastModified(Long.parseLong((String) value));
|
||||
try {
|
||||
target.setLastModified(Long.parseLong((String) value));
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
target.setLastModified(this.getFirstDate((String) value, LAST_MODIFIED));
|
||||
}
|
||||
}
|
||||
else {
|
||||
Class<?> clazz = (value != null) ? value.getClass() : null;
|
||||
@@ -842,9 +900,13 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
else if (IF_NONE_MATCH.equalsIgnoreCase(name)) {
|
||||
return source.getIfNoneMatch();
|
||||
}
|
||||
else if (IF_MODIFIED_SINCE.equalsIgnoreCase(name)) {
|
||||
long modifiedSince = source.getIfNotModifiedSince();
|
||||
return (modifiedSince > -1) ? modifiedSince : null;
|
||||
}
|
||||
else if (IF_UNMODIFIED_SINCE.equalsIgnoreCase(name)) {
|
||||
long unmodifiedSince = source.getIfNotModifiedSince();
|
||||
return (unmodifiedSince > -1) ? unmodifiedSince : null;
|
||||
String unmodifiedSince = source.getFirst(IF_UNMODIFIED_SINCE);
|
||||
return unmodifiedSince != null ? this.getFirstDate(unmodifiedSince, IF_UNMODIFIED_SINCE) : null;
|
||||
}
|
||||
else if (LAST_MODIFIED.equalsIgnoreCase(name)) {
|
||||
long lastModified = source.getLastModified();
|
||||
@@ -896,6 +958,27 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
return null;
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
private long getFirstDate(String headerValue, String headerName) {
|
||||
for (String dateFormat : DATE_FORMATS) {
|
||||
DateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US);
|
||||
simpleDateFormat.setTimeZone(GMT);
|
||||
try {
|
||||
return simpleDateFormat.parse(headerValue).getTime();
|
||||
}
|
||||
catch (ParseException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Cannot parse date value '" + headerValue +"' for '" + headerName + "' header");
|
||||
}
|
||||
|
||||
private String formatDate(long date) {
|
||||
DateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US);
|
||||
dateFormat.setTimeZone(GMT);
|
||||
return dateFormat.format(new Date(date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for creating a basic outbound mapper instance.
|
||||
@@ -922,4 +1005,5 @@ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanF
|
||||
mapper.setExcludedInboundStandardResponseHeaderNames(HTTP_RESPONSE_HEADER_NAMES_INBOUND_EXCLUSIONS);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans:beans xmlns="http://www.springframework.org/schema/integration/http"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/integration/http http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<beans:bean class="org.springframework.integration.http.inbound.UriPathHandlerMapping"/>
|
||||
|
||||
<inbound-gateway path="/test" request-channel="testChannel"
|
||||
payload-expression="T(org.springframework.web.context.request.RequestContextHolder).requestAttributes.request.queryString"/>
|
||||
|
||||
<int:publish-subscribe-channel id="testChannel"/>
|
||||
|
||||
<outbound-gateway id="proxyGateway" request-channel="testChannel"
|
||||
url-expression="'http://testServer/test?' + payload"/>
|
||||
|
||||
<int:bridge input-channel="testChannel" output-channel="checkHeadersChannel"/>
|
||||
|
||||
<int:channel id="checkHeadersChannel">
|
||||
<int:queue/>
|
||||
</int:channel>
|
||||
|
||||
</beans:beans>
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.http;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.net.URI;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.beans.PropertyAccessor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageHeaders;
|
||||
import org.springframework.integration.core.PollableChannel;
|
||||
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.servlet.HandlerAdapter;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
* @since 3.0
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class HttpProxyScenarioTests {
|
||||
|
||||
private final HandlerAdapter handlerAdapter = new HttpRequestHandlerAdapter();
|
||||
|
||||
@Autowired
|
||||
private HandlerMapping handlerMapping;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("proxyGateway.handler")
|
||||
private HttpRequestExecutingMessageHandler handler;
|
||||
|
||||
@Autowired
|
||||
private PollableChannel checkHeadersChannel;
|
||||
|
||||
@Test
|
||||
public void testHttpProxyScenario() throws Exception {
|
||||
DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
final long ifModifiedSince = c.getTimeInMillis();
|
||||
String ifModifiedSinceValue = dateFormat.format(ifModifiedSince);
|
||||
|
||||
c.add(Calendar.DATE, -1);
|
||||
long ifUnmodifiedSince = c.getTimeInMillis();
|
||||
final String ifUnmodifiedSinceValue = dateFormat.format(ifUnmodifiedSince);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test");
|
||||
request.setQueryString("foo=bar&FOO=BAR");
|
||||
|
||||
request.addHeader("If-Modified-Since", ifModifiedSinceValue);
|
||||
request.addHeader("If-Unmodified-Since", ifUnmodifiedSinceValue);
|
||||
|
||||
Object handler = this.handlerMapping.getHandler(request).getHandler();
|
||||
assertNotNull(handler);
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
RestTemplate template = Mockito.spy(new RestTemplate());
|
||||
|
||||
Mockito.doAnswer(new Answer<ResponseEntity<?>>() {
|
||||
@Override
|
||||
public ResponseEntity<?> answer(InvocationOnMock invocation) throws Throwable {
|
||||
URI uri = (URI) invocation.getArguments()[0];
|
||||
assertEquals(new URI("http://testServer/test?foo=bar&FOO=BAR"), uri);
|
||||
HttpEntity<?> httpEntity = (HttpEntity) invocation.getArguments()[2];
|
||||
HttpHeaders httpHeaders = httpEntity.getHeaders();
|
||||
assertEquals(ifModifiedSince, httpHeaders.getIfNotModifiedSince());
|
||||
assertEquals(ifUnmodifiedSinceValue, httpHeaders.getFirst("If-Unmodified-Since"));
|
||||
return new ResponseEntity<Object>(httpEntity.getHeaders(), HttpStatus.OK);
|
||||
}
|
||||
}).when(template).exchange(Mockito.any(URI.class), Mockito.any(HttpMethod.class),
|
||||
Mockito.any(HttpEntity.class), (Class<?>) Mockito.any(Class.class));
|
||||
|
||||
PropertyAccessor dfa = new DirectFieldAccessor(this.handler);
|
||||
dfa.setPropertyValue("restTemplate", template);
|
||||
|
||||
RequestAttributes attributes = new ServletRequestAttributes(request);
|
||||
RequestContextHolder.setRequestAttributes(attributes);
|
||||
|
||||
this.handlerAdapter.handle(request, response, handler);
|
||||
|
||||
assertNull(response.getHeaderValue("If-Modified-Since"));
|
||||
assertNull(response.getHeaderValue("If-Unmodified-Since"));
|
||||
|
||||
Message<?> message = this.checkHeadersChannel.receive(2000);
|
||||
MessageHeaders headers = message.getHeaders();
|
||||
|
||||
assertEquals(ifModifiedSince, headers.get("If-Modified-Since"));
|
||||
assertEquals(ifUnmodifiedSince, headers.get("If-Unmodified-Since"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import java.net.URI;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -48,6 +49,7 @@ import org.springframework.util.CollectionUtils;
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Mark Fisher
|
||||
* @author Gunnar Hillert
|
||||
* @author Artem Bilan
|
||||
* @since 2.0.1
|
||||
*/
|
||||
public class DefaultHttpHeaderMapperFromMessageInboundTests {
|
||||
@@ -549,13 +551,27 @@ public class DefaultHttpHeaderMapperFromMessageInboundTests {
|
||||
HeaderMapper<HttpHeaders> mapper = DefaultHttpHeaderMapper.inboundMapper();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
// suppressed in response on inbound, by default
|
||||
headers.put("Content-Length", Arrays.asList(new String[] {"3"}));
|
||||
headers.put("Content-Length", Arrays.asList("3"));
|
||||
Map<String, Object> messageHeaders = mapper.toHeaders(headers);
|
||||
headers = new HttpHeaders();
|
||||
mapper.fromHeaders(new MessageHeaders(messageHeaders), headers);
|
||||
assertNull(headers.get("Content-Length"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInt2995IfModifiedSince() throws Exception{
|
||||
HeaderMapper<HttpHeaders> mapper = DefaultHttpHeaderMapper.inboundMapper();
|
||||
Date ifModifiedSince = new Date();
|
||||
long ifModifiedSinceTime = ifModifiedSince.getTime();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setIfModifiedSince(ifModifiedSinceTime);
|
||||
Map<String, ?> result = mapper.toHeaders(headers);
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.setTime(ifModifiedSince);
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
assertEquals(c.getTimeInMillis(), result.get("If-Modified-Since"));
|
||||
}
|
||||
|
||||
public static class TestClass {
|
||||
|
||||
}
|
||||
|
||||
@@ -25,17 +25,22 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.UnsupportedCharsetException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageHeaders;
|
||||
import org.springframework.integration.mapping.HeaderMapper;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
@@ -668,4 +673,19 @@ public class DefaultHttpHeaderMapperFromMessageOutboundTests {
|
||||
assertEquals(0, messageHeaders.size());
|
||||
}
|
||||
|
||||
public void testInt2995IfModifiedSince() throws Exception{
|
||||
Date ifModifiedSince = new Date();
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.US);
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
String value = dateFormat.format(ifModifiedSince);
|
||||
Message<?> testMessage = MessageBuilder.withPayload("foo").setHeader("If-Modified-Since", value).build();
|
||||
HeaderMapper<HttpHeaders> mapper = DefaultHttpHeaderMapper.outboundMapper();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
mapper.fromHeaders(testMessage.getHeaders(), headers);
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.setTime(ifModifiedSince);
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
assertEquals(c.getTimeInMillis(), headers.getIfNotModifiedSince());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -213,6 +213,7 @@
|
||||
For more information see <xref linkend="chain"/>.
|
||||
</para>
|
||||
</section>
|
||||
<<<<<<< HEAD
|
||||
<section id="3.0-jms-mdca-te">
|
||||
<title>JMS Message Driven Channel Adapter</title>
|
||||
<para>
|
||||
@@ -326,5 +327,15 @@
|
||||
For more information see <xref linkend="delayer"/>.
|
||||
</para>
|
||||
</section>
|
||||
<section id="3.0-http-header-mapper">
|
||||
<title>DefaultHttpHeaderMapper and 'If-(Un)Modified-Since' HTTP headers</title>
|
||||
<para>
|
||||
Previously, 'If-Modified-Since' and 'If-Unmodified-Since' HTTP headers were incorrectly processed
|
||||
within from/to HTTP headers mapping in the <classname>DefaultHttpHeaderMapper</classname>.
|
||||
Now, in addition to the fix of that issue, <classname>DefaultHttpHeaderMapper</classname> provides date parsing
|
||||
from formatted strings for HTTP headers, which accept date-time values.
|
||||
For more information see <xref linkend="http"/>.
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
Reference in New Issue
Block a user