Skip to content

Use RFC 822 format for marshalling date shapes #3452

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

Merged
merged 2 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavav2-83d35fd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "AWS SDK for Java v2",
"contributor": "dave-fn",
"type": "bugfix",
"description": "Build headers with two-digit day of month to meet RFC 822 reporting requirement"
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public void assertSkewed(int clientSideTimeOffset,
int statusCode,
Instant serverDate) {
AwsServiceException exception = exception(clientSideTimeOffset, errorCode, statusCode,
DateUtils.formatRfc1123Date(serverDate));
DateUtils.formatRfc822Date(serverDate));
assertThat(exception.isClockSkewException()).isTrue();
}

Expand All @@ -96,7 +96,7 @@ public void assertNotSkewed(int clientSideTimeOffset,
int statusCode,
Instant serverDate) {
AwsServiceException exception = exception(clientSideTimeOffset, errorCode, statusCode,
DateUtils.formatRfc1123Date(serverDate));
DateUtils.formatRfc822Date(serverDate));
assertThat(exception.isClockSkewException()).isFalse();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ private Consumer<RetryPolicyContext.Builder> applyErrorCode(String errorCode) {

private Consumer<RetryPolicyContext.Builder> applyErrorCode(String errorCode, Duration clockSkew, Instant dateHeader) {
SdkHttpFullResponse response = SdkHttpFullResponse.builder()
.putHeader("Date", DateUtils.formatRfc1123Date(dateHeader))
.putHeader("Date", DateUtils.formatRfc822Date(dateHeader))
.build();

AwsErrorDetails errorDetails = AwsErrorDetails.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public void marshall(Boolean val, StructuredJsonGenerator jsonGenerator, JsonMar
jsonGenerator.writeNumber(DateUtils.formatUnixTimestampInstant(val));
break;
case RFC_822:
jsonGenerator.writeValue(DateUtils.formatRfc1123Date(val));
jsonGenerator.writeValue(DateUtils.formatRfc822Date(val));
break;
case ISO_8601:
jsonGenerator.writeValue(DateUtils.formatIso8601Date(val));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public String convert(Instant val, SdkField<Instant> sdkField) {
case ISO_8601:
return DateUtils.formatIso8601Date(val);
case RFC_822:
return DateUtils.formatRfc1123Date(val);
return DateUtils.formatRfc822Date(val);
case UNIX_TIMESTAMP:
return DateUtils.formatUnixTimestampInstant(val);
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public Instant convert(String value, SdkField<Instant> field) {
case UNIX_TIMESTAMP_MILLIS:
return safeParseDate(DateUtils::parseUnixTimestampMillisInstant).apply(value);
case RFC_822:
return DateUtils.parseRfc1123Date(value);
return DateUtils.parseRfc822Date(value);
default:
throw SdkClientException.create("Unrecognized timestamp format - " + format);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public static Optional<Instant> getServerTime(SdkHttpResponse serviceResponse) {
log.debug(() -> "Reported service date: " + serverDate);

try {
return Optional.of(DateUtils.parseRfc1123Date(serverDate));
return Optional.of(DateUtils.parseRfc822Date(serverDate));
} catch (RuntimeException e) {
log.warn(() -> "Unable to parse clock skew offset from response: " + serverDate, e);
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void badDateTranslatesToZero() {

private SdkHttpFullResponse responseWithDateOffset(int value, ChronoUnit unit) {
return SdkHttpFullResponse.builder()
.putHeader("Date", DateUtils.formatRfc1123Date(Instant.now().plus(value, unit)))
.putHeader("Date", DateUtils.formatRfc822Date(Instant.now().plus(value, unit)))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private void stubForResponse(Instant serviceTime, int statusCode, String errorCo
.willReturn(aResponse()
.withStatus(statusCode)
.withHeader("x-amzn-ErrorType", errorCode)
.withHeader("Date", DateUtils.formatRfc1123Date(serviceTime))
.withHeader("Date", DateUtils.formatRfc822Date(serviceTime))
.withBody("{}")));
}

Expand All @@ -180,7 +180,7 @@ private void stubForClockSkewFailureThenSuccess(Instant serviceTime, int statusC
.willReturn(aResponse()
.withStatus(statusCode)
.withHeader("x-amzn-ErrorType", errorCode)
.withHeader("Date", DateUtils.formatRfc1123Date(serviceTime))
.withHeader("Date", DateUtils.formatRfc822Date(serviceTime))
.withBody("{}")));

stubFor(post(urlEqualTo(PATH))
Expand Down
44 changes: 44 additions & 0 deletions utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.annotations.ThreadSafe;

Expand All @@ -48,6 +51,20 @@ public final class DateUtils {
.toFormatter()
.withZone(UTC);

/**
* RFC 822 date/time formatter.
*/
static final DateTimeFormatter RFC_822_DATE_TIME = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.parseLenient()
.appendPattern("EEE, dd MMM yyyy HH:mm:ss")
.appendLiteral(' ')
.appendOffset("+HHMM", "GMT")
.toFormatter()
.withLocale(Locale.US)
.withResolverStyle(ResolverStyle.SMART)
.withChronology(IsoChronology.INSTANCE);

// ISO_INSTANT does not handle offsets in Java 12-. See https://bugs.openjdk.java.net/browse/JDK-8166138
private static final List<DateTimeFormatter> ALTERNATE_ISO_8601_FORMATTERS =
Arrays.asList(ISO_INSTANT, ALTERNATE_ISO_8601_DATE_FORMAT, ISO_OFFSET_DATE_TIME);
Expand Down Expand Up @@ -102,6 +119,33 @@ public static String formatIso8601Date(Instant date) {
return ISO_INSTANT.format(date);
}

/**
* Parses the specified date string as an RFC 822 date and returns the Date object.
*
* @param dateString
* The date string to parse.
*
* @return The parsed Date object.
*/
public static Instant parseRfc822Date(String dateString) {
if (dateString == null) {
return null;
}
return parseInstant(dateString, RFC_822_DATE_TIME);
}

/**
* Formats the specified date as an RFC 822 string.
*
* @param instant
* The instant to format.
*
* @return The RFC 822 string representing the specified date.
*/
public static String formatRfc822Date(Instant instant) {
return RFC_822_DATE_TIME.format(ZonedDateTime.ofInstant(instant, UTC));
}

/**
* Parses the specified date string as an RFC 1123 date and returns the Date
* object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
import static java.time.ZoneOffset.UTC;
import static java.time.format.DateTimeFormatter.ISO_INSTANT;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static software.amazon.awssdk.utils.DateUtils.ALTERNATE_ISO_8601_DATE_FORMAT;
import static software.amazon.awssdk.utils.DateUtils.RFC_822_DATE_TIME;

import java.text.ParseException;
import java.text.SimpleDateFormat;
Expand Down Expand Up @@ -72,6 +75,80 @@ public void formatIso8601Date() throws ParseException {
assertEquals(expectedDate, actualDate);
}

@Test
public void formatRfc822Date_DateWithTwoDigitDayOfMonth_ReturnsFormattedString() throws ParseException {
String string = DateUtils.formatRfc822Date(INSTANT);
Instant parsedDateAsInstant = LONG_DATE_FORMAT.parse(string).toInstant();
assertThat(parsedDateAsInstant).isEqualTo(INSTANT);
}

@Test
public void formatRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsFormattedString() throws ParseException {
Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);;
String string = DateUtils.formatRfc822Date(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH);
Instant parsedDateAsInstant = LONG_DATE_FORMAT.parse(string).toInstant();
assertThat(parsedDateAsInstant).isEqualTo(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH);
}

@Test
public void formatRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsStringWithZeroLeadingDayOfMonth() throws ParseException {
final Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);;
String string = DateUtils.formatRfc822Date(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH);
String expectedString = "Wed, 07 May 2014 17:43:26 GMT";
assertThat(string).isEqualTo(expectedString);
}

@Test
public void parseRfc822Date_DateWithTwoDigitDayOfMonth_ReturnsInstantObject() throws ParseException {
String formattedDate = LONG_DATE_FORMAT.format(Date.from(INSTANT));
Instant parsedInstant = DateUtils.parseRfc822Date(formattedDate);
assertThat(parsedInstant).isEqualTo(INSTANT);
}

@Test
public void parseRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsInstantObject() throws ParseException {
final Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);;
String formattedDate = LONG_DATE_FORMAT.format(Date.from(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH));
Instant parsedInstant = DateUtils.parseRfc822Date(formattedDate);
assertThat(parsedInstant).isEqualTo(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH);
}

@Test
public void parseRfc822Date_DateWithInvalidDayOfMonth_IsParsedWithSmartResolverStyle() {
String badDateString = "Wed, 31 Apr 2014 17:43:26 GMT";
String validDateString = "Wed, 30 Apr 2014 17:43:26 GMT";
Instant badDateParsedInstant = DateUtils.parseRfc822Date(badDateString);
Instant validDateParsedInstant = DateUtils.parseRfc1123Date(validDateString);
assertThat(badDateParsedInstant).isEqualTo(validDateParsedInstant);
}

@Test
public void parseRfc822Date_DateWithInvalidDayOfMonth_MatchesRfc1123Behavior() {
String dateString = "Wed, 31 Apr 2014 17:43:26 GMT";
Instant parsedInstantFromRfc822Parser = DateUtils.parseRfc822Date(dateString);
Instant parsedInstantFromRfc1123arser = DateUtils.parseRfc1123Date(dateString);
assertThat(parsedInstantFromRfc822Parser).isEqualTo(parsedInstantFromRfc1123arser);
}

@Test
public void parseRfc822Date_DateWithDayOfMonthLessThan10th_MatchesRfc1123Behavior() {
String rfc822DateString = "Wed, 02 Apr 2014 17:43:26 GMT";
String rfc1123DateString = "Wed, 2 Apr 2014 17:43:26 GMT";
Instant parsedInstantFromRfc822Parser = DateUtils.parseRfc822Date(rfc822DateString);
Instant parsedInstantFromRfc1123arser = DateUtils.parseRfc1123Date(rfc1123DateString);
assertThat(parsedInstantFromRfc822Parser).isEqualTo(parsedInstantFromRfc1123arser);
}

@Test
public void resolverStyleOfRfc822FormatterMatchesRfc1123Formatter() {
assertThat(RFC_822_DATE_TIME.getResolverStyle()).isSameAs(RFC_1123_DATE_TIME.getResolverStyle());
}

@Test
public void chronologyOfRfc822FormatterMatchesRfc1123Formatter() {
assertThat(RFC_822_DATE_TIME.getChronology()).isSameAs(RFC_1123_DATE_TIME.getChronology());
}

@Test
public void formatRfc1123Date() throws ParseException {
String string = DateUtils.formatRfc1123Date(INSTANT);
Expand All @@ -84,7 +161,7 @@ public void formatRfc1123Date() throws ParseException {
}

@Test
public void parseRfc822Date() throws ParseException {
public void parseRfc1123Date() throws ParseException {
String formatted = LONG_DATE_FORMAT.format(Date.from(INSTANT));
Instant expected = LONG_DATE_FORMAT.parse(formatted).toInstant();
Instant actual = DateUtils.parseRfc1123Date(formatted);
Expand Down